Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/philipnilsson/bueno

Composable validators for forms, API:s in TypeScript
https://github.com/philipnilsson/bueno

Last synced: about 2 months ago
JSON representation

Composable validators for forms, API:s in TypeScript

Awesome Lists containing this project

README

        





A tiny, composable validation library. Bueno primary aims to be an
improvement on form validation libraries like
[`yup`](https://github.com/jquense/yup) and
[`superstruct`](https://github.com/ianstormtaylor/superstruct), but
can also be used as a lightweight API validation library. You'll like
it if you need something

🌳 Small & tree-shakeable.

💡 Expressive! Use full boolean logic to compose your schemas

💫 Bidirectional. Learn
more

🚀 Awesome error messages in multiple languages
supported out of the box, with more on the way. Learn more

⏱ Asynchronous
(when needed!)

# Try it out

You can check out `bueno` directly in the browser in this
[jsfiddle](https://jsfiddle.net/gm1pbk3e/11/).

# Installation

Install using `npm install --save bueno` or `yarn add bueno`.

Check out the quickstart section below, or go directly to the API docs

# Quickstart

`bueno` allows you to quickly and predictably compose validation
schemas. Here's how it looks in action:

```typescript
import { alphaNumeric, atLeast, check, checkPerKey, deDE, describePaths, either, email, enUS, length, moreThan, not, number, object, optional, string, svSE } from 'bueno'

const username =
string(length(atLeast(8)), alphaNumeric)

const age =
number(atLeast(18), not(moreThan(130)))

const user = object({
id: either(email, username),
age: optional(age)
})

const input = {
id: '[email protected]',
age: 17
}

console.log(check(input, user, enUS))
// 'Age must be at least 18 or left out'

console.log(check(input, user, describePaths(svSE, [['age', 'Ålder']])))
// 'Ålder måste vara som minst 18'

console.log(checkPerKey(input, user, deDE))
// { age: 'Muss mindestens 18 sein' }
```

[Try this example in a Fiddle](https://jsfiddle.net/o9urhv3m/2/)

# API documentation

## Core

Schemas are constructed using basic
schemas
like `number` `string`, `atLeast(10)`, `exactly(null)` and
by using combinators like `either`,
`object`, `array`, `fix` to create more complex schemas.

Most schemas (specifically `Schema_`:s) can be called as
functions with other schemas as arguments. E.g.

```typescript
number(even, atLeast(10))
```

The semantics are a schema returning the value of `number` with the
additional validations from `even` and `atLeast(10)` taking place.

## Running a schema

[check](#check) • [checkPerKey](#check) • [result](#check)


The following functions allow you to feed input into a schema to parse
& validate it. Note that schema evaluation is cached, so calling e.g.
`check(input)` then immediately `result(input)` is not inefficient.

### `check`

```java
checkAsync :: (value : A, schema : Schema, locale : Locale) : string | null
```

Returns a `string` with a validation error constructed using the given
locale, or `null` if validation succeeded.

```typescript
check('123', number, enUS)
// 'Must be a number'
```

### `checkByKey`

Returns an object of errors for each key in an object (for a schema
constructed using the [`object`](#object) combinator)

```typescript
checkByKey({ n: '123', b: true }, object({ n: number, b: boolean }, enUS)
// { n: 'Must be a number', b: 'Must be a boolean' }
```

### `result`

Returns the result of parsing using a schema.

```typescript
result({ n: '123', d: 'null' }, object({ n: toNumber, d: toJSON })
// { n: 123, d: null }
```

### `checkAsync`, `checkByKeyAsync` and `resultAsync`

The async versions of `check`, `checkByKey` and `result` respectively.

## Combinator API

apply
both
compact
defaultTo
either
every
fix
flip
not
object
optional
pipe
self
setMessage
some
when

Combinators create new, more complex schemas out of existing, simpler schemas.

### `both`

Creates a schema that satisfies both of its arguments.

```java
both :: (v : Schema, w : Schema,) => Schema_
```

```typescript
const schema =
both(even, atLeast(10))

check(schema, 11, enUS)
// 'Must be even.'

check(schema, 8, enUS)
// 'Must be at least 10.'

check(schema, 12, enUS)
// null
```

You may prefer using the call signatures
of schemas over using this combinator.

### `either`

Creates a schema that satisfies either of its arguments.

```java
either :: (v : Schema, w : Schema,) => Schema_
```

```typescript
const schema =
either(even, atLeast(10))

check(schema, 11, enUS)
// null

check(schema, 8, enUS)
// null

check(schema, 9, enUS)
// 'Must be even or at least 10'
```

### `optional`

Make a schema also match `undefined`.

```java
optional ::
(v : Schema) : Schema
```

```typescript
const schema = optional(number)

check(schema, 9, enUS)
// null

check(schema, undefined, enUS)
// null

check(schema, null, enUS)
// 'Must be a number or left out
```

### `not`

```java
not ::
(v : Schema) => Schema_
```

Negates a schema. Note that negation only affect the "validation" and
not the "parsing" part of a schema. Essentially, remember that `not` does not affect
the type signature of a schema.

For example, `not(number)` is the same as just `number`. The reason is
that we can't really do much with a value that we know only to have
type "not a number".

```typescript
const schema =
number(not(moreThan(100)))

check(103, schema, enUS)
// Must not be more than 100
```

### `object`

Create a schema on objects from an object of schemas.

```java
object :: (vs :
{ [Key in keyof AS]: Schema } &
{ [Key in keyof BS]: Schema }
) => Schema_
```

```typescript
const schema = object({
age: number,
name: string
})

check({ age: 13 }, schema, enUS)
// Name must be a string

check({ age: '30', name: 'Philip' }, schema, enUS)
// Age must be a number

check({ age: 30, name: 'Philip' }, schema, enUS)
// null
```

You can use [`compact`](#compact) to make undefined keys optional.

`inexactObject` and `exactObject` are versions of this that are more
lenient / strict w.r.t keys not mentioned in the schema.

### `compact`

Remove keys in an object that are `undefined`.

```java
compact ::
(p : Schema) : Schema_, UndefinedOptional> {
```

```typescript
const schema =
compact(object({ n: optional(number) }))

result({ n: undefined }, schema)
// {}
```

### `fix`

Create a schema that can recursively be defined in terms
itself. Useful for e.g. creating a schema that matches a binary tree
or other recursive structures.

```java
fix ::
(fn : (v : Schema) => Schema) => Schema_
```

TypeScript is not too great at inferring types using this combinators,
so typically help it using an annotation as below

```typescript
type BinTree
= {
left : BinTree
| null,
right : BinTree
| null,
value : A
}

const bintree = fix, BinTree>(bintree => object({
left: either(exactly(null), bintree),
right: either(exactly(null), bintree),
value: toNumber
}))
```

### `self`

Create a schema dynamically defined in terms of its input.

```typescript
type User = {
verified : boolean,
email : string | null
}

const schema = self(user => {
return object({
verified: boolean,
email: user.verified ? email : exactly(null)
})
})
```

### `flip`

Reverse a schema

```java
flip ::
(schema : Schema) => Schema_
```

```typescript
const schema = reverse(toNumber)

result(123, schema)
// '123'
```

### `defaultTo`

Set a default value for a schema when it fails parsing.

```java
defaultTo ::
(b: B, schema : Schema) => Schema_
```

```typescript
const schema =
defaultTo(100, number)

result(null, schema)
// 100
```

### `pipe`

Pipe the output of a schema as the input into another

```java
pipe ::
(s : Schema, t : Schema) => Schema_
```

```typescript
const schema =
pipe(toNumber, lift(x => x + 1))

result('123', schema)
// 124
```

### `apply`

Set the input of a schema to a fixed value. Can be used when creating
a schema where the definition of one key depends on another.

```java
apply ::
(v : Schema, value : A, path : string) => Schema_;
```

```typescript
type Schedule = {
weekday : string
price : number
}

const schema = self((schedule : Schedule) => object({
weekday: oneOf('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'),
price: when(
// When schedule.weekday is Sat or Sun, the
// price must be at least 100, otherwise at most 50.
apply(oneOf('Sat', 'Sun'), schedule.weekday, 'weekday'),
atLeast(100),
atMost(50)
)
})
```

### `every`

A variable arguments version of `both`.

```java
every ::
(...vs : Schema[]) => Schema_
```

### `setMessage`

Set the error message of a parser.

```typescript
const thing = setMessage(
object({ foo: string, bar: number }),
l => l.noun('should be a thingy),
)

check({ foo: '' } as any, thing, enUS)
// 'Should be a thingy'
```

### `some`

A variable arguments version of `either`.

```java
some ::
(...vs : Schema[]) => Schema_
```

### `when`

"if-then-else" on parsers.

```java
when ::
(cond : Schema, consequent : Schema, alternative : Schema) => Schema_
```

The "else" part is optional, in which case this combinator has the signature

```java
when ::
(cond : Schema, consequent : Schema) => Schema_
```

```typescript
const schema =
when(even, atLeast(10), atMost(20))

check(8, schema, enUS)
// 'Must be at least 10 when even'

check(21, schema, enUS)
// 'Must be at most 20 when not even'

check(11, schema, enUS)
// null
```

## Basic schemas

alphaNumeric
any
atLeast
atMost
between
boolean
date
email
emptyString
even
exactly
id
integer
length
lessThan
lift
match
moreThan
number
objectExact
objectInexact
odd
oneOf
optionalTo
pair
path
size
string
sum
swap
toDate
toJSON
toNumber
toString
toURL
unknown

Basic schemas are simple schemas that can be composed into more
complex ones using the combinator API.

### `alphaNumeric`

```java
alphaNumeric :: Schema_
```

Match an alphanumeric string.

```typescript
check('acb123', alphaNumeric, enUS)
// null

check('acb|123', alphaNumeric, enUS)
// Must have letters and numbers only
```

### `any`

```java
any :: Schema_
```

Successfully matches any input.

```typescript
check(123, any, enUS)
// null

check(undefined, any, enUS)
// null
```

### `atLeast`

```java
atLeast :: (lb : A) => Schema_
```

Matches a value at least as big as the provided lower bound `lb`.

```typescript
const schema =
atLeast(100)

check(88, schema, enUS)
// 'Must be at least 100'
```

### `atMost`

```java
atMost ::
(ub : A) => Schema_
```

Matches a value at most as big as the provided upper bound `ub`.

```typescript
const schema =
atMost(100)

check(88, schema, enUS)
// 'Must be at most 100'
```

### `between`

Matches a value between the provided lower and upper bounds (inclusive)

```java
between :: (lb : number, ub : number) => Schema_
```

```typescript
check(99, schema, enUS)
// 'Must be between 100 and 200'

check(201, schema, enUS)
// 'Must be between 100 and 200'

check(100, schema, enUS)
// null

check(200, schema, enUS)
// null
```

### `boolean`

Matches a boolean.

```java
boolean :: Schema_
```

### `date`

Matches a `Date` object.

```java
date :: Schema_
```

### `email`

Matches an email (validated using a permissive regular expression)

```java
email :: Schema_
```

### `emptyString`

Matches the empty string

```java
emptyString :: Schema_
```

### `even`

Matches an even number

```java
even :: Schema_
```

### `exactly`

Creates a schema that matches a single value, optionally using an equality comparison operator.

```java
exactly ::
(target : A, equals : (x : A, y : A) => boolean = (x, y) => x === y) => Schema_
```

```typescript
check('abc', exactly('abc'), enUS)
// null

check('abd', exactly('abc'), enUS)
// 'Must be abc'

check('abd', exactly('abc', (x, y) => x.length === y.length), enUS)
// null
```

### `id`

```java
id ::
() => Schema_
```

The identity schema that always succeeds. Unlike `any`, `id` can be provided a type
argument other than the `any` type.

```typescript
const schema =
object({ foo: id() })

check({ foo: 123 }, schema, enUS)
// null

check({ foo: 'hi!' }, schema, enUS)
// evaluates to null, but has a type error
```

### `integer`

Match a whole number

```java
integer :: Schema_
```

### `length`

Match an object with property length matching the schema argument

```java
length ::
(...vs : Schema[]) => Schema_
```

```typescript
const username =
string(length(exactly(10)))

const items =
array(between(1, 10))
```

### `lessThan`

Match a number less than the provided upper bound `ub`

```java
lessThan :: (ub : number) => Schema_
```

### `lift`

Lift a function into a schema that uses the function for parsing.

```typescript
const schema =
pipe(toNumber, lift(x => x + 1))

result('123', schema)
// 124
```

### `match`

Match a string matching the provided regular expression.

```typescript
const greeting =
match(/Hello|Hi|Hola/, m => m.mustBe('a greeting'))

check('Hello', greeting, enUS)
// null

check('Yo', greeting, enUS)
// 'Must be a greeting'
```

### `moreThan`

Match a number more than the provided lower bound `lb`

```java
moreThan :: (lb : number) => Schema_
```

### `number`

Match any number

```java
number :: Schema_
```

### `objectExact`

Like `object` but match the object exactly, i.e. error if additional
keys to the ones specified are present.

### `objectInexact`

Like `object` but match the object inexactly, i.e. whereas `object`
will silently remove any keys not specified in the schema,
`objectInexact` will keep them.

### `odd`

Matches an odd number

```java
odd :: Schema_
```

### `oneOf`

Match exactly one of the given elements

```typescript
const weekend =
oneOf('Fri', 'Sat', 'Sun'),

check('Sat', weekend, enUS)
// null

check('Wed', weekend, enUS)
// Must be Fri, Sat or Sun
```

### `swap`

Swap elements

```java
swap ::
(dict : [[A, A]]) => Schema_
```

```typescript
const optionalToEmptyString =
swap([[undefined, ''], [null, '']])

result(null, optionalToEmptyString)
// ''

result(undefined, optionalToEmptyString)
// ''

result('foo', optionalToEmptyString)
// 'foo'
```

### `optionalTo`

Map `null` and `undefined` to another value.

```java
optionalTo ::
(to : A) => Schema;
```

```typescript
const schema =
pipe(optionalTo(''), length(atMost(3)))

result(null, schema)
// ''

check(null, schema, enUS)
// null'

check('123123', schema, enUS)
// 'Must have length at most 3.'
```

### `pair`

Create a schema for pairs or values from a pair of schemas (where a
pair is a typed two-element array)

```java
pair ::
(v : Schema,w : Schema) => Schema_<[A, B], [C, D]>
```

```typescript
const schema =
pair(toNumber, toDate)

result(['123', '2019-12-12'], schema)
// [ 123, 2019-12-12T00:00:00.000Z ]
```

### `path`

Set the `path` that a schema reports errors at.

```java
path ::
(path : string, v : Schema) => Schema
```

```typescript
const schema =
path('foo', number)

check('', schema, enUS)
// 'Foo must be a number'
```

### `size`

`size` is the same as `length` except using the `size` property. Usable
for sets etc.

```java
size ::
(...vs : Schema[]) => Schema_
```

### `string`

Match a string

```java
string :: Schema_
```

### `sum`

Match an array with sum matching the schema argument.

```java
sum :: (...vs : Schema[]) => Schema_
```

```typescript
const schema =
sum(atLeast(10))

check([1,2,3], schema, enUS)
// Must have sum at least 10
```

### `toDate`

Convert a string to a date. Simply parses the string using the date
constructor which can be unreliable, so you may want to use [date-fns](https://date-fns.org/) instead.

```java
toDate :: Schema_
```

### `toJSON`

Converts a string to JSON.

```java
toJSON :: Schema_
```

### `toNumber`

Converts a string to a number.

```java
toNumber :: Schema_
```

### `toString`

Converts a value to a string.

```java
toString :: Schema_
```

### `toURL`

Converts a string to an [URL](https://developer.mozilla.org/en-us/docs/Web/API/URL) object.

### `unknown`

Successfully parses a value (same as `any`) but types it as unknown.

## Collection related schemas

array
iterable
map
set
toArray
toMap
toMapFromObject
toSet

### `array`

Create a schema on arrays from a schema on its values.

```java
array :: (v : Schema,) : Schema_
```

```typescript
const schema =
array(toNumber)

result(['1', '2', '3'], schema)
// [1, 2, 3]

check(['1', '2', true], schema, enUS)
// Element #3 must be a string
```
### `iterable`

Match any `Iterable` value.

```java
iterable ::
() => Schema_, Iterable>
```

```typescript
const schema =
iterable()

check(['hello', 'world'], schema, enUS)
// null
```

### `map`

Create a schema that matches a `Map` from schemas describing the keys
and values respectively.

```
map ::
(k : Schema, v : Schema) => Schema_, Map>
```

```typescript
const schema =
map(number(atLeast(10)), string(length(atLeast(1))))

check(new Map([[1, 'a'], [2, 'b'], [3, 'c']]), schema, enUS)
// Element #1.key must be at least 10

check(new Map([[11, 'a'], [12, 'b'], [13, '']]), schema, enUS)
// Element #3.value must have length at least 1

check(new Map([[11, 'a'], [12, 'b'], [13, 'c']]), schema, enUS)
// null
```

### `set`

Create a schema for a Set from a schema describing the values of the set.

```java
set ::
(v : Schema) => Schema_, Set>
```

```typescript
const schema =
set(any)

check(new Set([1, 'a', true], schema, enUS)
// null

check([1, 'a', true], schema, enUS)
// 'Must be a set'
```

```typescript
const schema =
set(toNumber)

parse(new Set(['1', '2', '3']))
// Set(3) { 1, 2, 3 }
```

### `toArray`

Convert an iterable to an array.

```java
toArray ::
() => Schema_, A[]>
```

### `toMap`

Convert an iterable of pairs to a Map

```java
toMap ::
() => Schema_, Map>
```

### `toMapFromObject`

Convert an objet into a Map.

```java
toMapFromObject ::
() : Schema_<{ [key in A]: B }, Map>
```

```typescript
result({ 'a': 3, 'b': 10, 'c': 9 }, toMapFromObject())
// Map(3) { 'a' => 3, 'b' => 10, 'c' => 9 }
```

It only works on "real" objects.

```typescript
check('', toMapFromObject(), enUS)
// 'Must be an object'
```

### `toSet`

Convert an iterable of values into a set.

```java
toSet ::
() : Schema_, Set>
```

## Factory functions

Factory functions let you create new schema definitions.

### `createSchema`

```java
createSchema ::
(
parse : SchemaFactory
,
unparse : SchemaFactory = irreversible('createSchema')
) : Schema_

```

Create a schema from two "parser factories" for each "direction" of
parsing. (See [Bidirectionality](./src/docs/bidirectionali.md).) A
single factory may be provided, but the schema will not be invertible.

A `SchemaFactory` is simply a function of type
```typescript
type SchemaFactory
= (a : A) => Action<{
parse?: { ok : boolean, msg : string | Message },
validate?: { ok : boolean, msg : string | Message },
result?: B,
score?: number
}>
```

All properties are optional and described below.

##### `result`

Provide this if the schema performs any parsing. This is the result
value of the parsing. A schema performs parsing when it transforms the
input of the schema into something else, e.g. by transforming a string
representation of a date into a `Date`-object.

##### `parse`

Provide this if the schema is doing any parsing. (See `result`)

The `ok` parameter indicates whether the parse was
successful. `message` is the error message describing what the parser does.

##### `validate`

Provide this if the schema is doing any validation. The `ok` parameter
indicates whether the validation was successful. `message` is the
error message describing what the parser does.

##### `score`

The score is used by `bueno` to generate better error messages in
certain situations. You're most likely fine not providing it.

You may however optionally proide a `score` between 0 and 1 to
indicate how successful the schema was. This will by default be either
0 or 1 depending on whether the schema successfully handled its input
or not.

An example of a schema that uses a non-binary score is
`array(number)`. If we ask this schema to handle the input
`[1,2,3,4,'5']` it will be ranked with a score of 4/5.

Here's an example, creating a schema matching a "special" number.

```typescript
const special = number(createSchema(
async function(a : number) {
// `createSchema` may be async.
await new Promise(k => setTimeout(k, 100))
return {
validate: {
ok: [2, 3, 5, 7, 8].indexOf(a) >= 0,
msg: (l : Builder) => l.mustBe('a special number')>
},
// Transform the special number into a string.
result: '' + a
}
}
))
```

## Types

### `Schema`

The type of a schema. Converts a value of type `A` into one of type `B`
and validates the result.

### `Schema_`

A `Schema` that can be used with "call syntax". An example of a
`Schema_` is `number`, and it can be enhanced by calling it
with additional arguments.

```typescript
const schema =
number(even, atLeast(12))
```

### `Action`

An `Action` is either an `A` or a `Promise`. A schema returning a
`Promise` will be asynchronous.

### `Builder`

A builder is an object that contains methods for building error
messages when using type-safe i18n. See [customizing
errors](./src/docs/customizing-errors.md)

### `Message`

This type is used to create error messages that are independent of a
specific locale.

It is a value of type `(l : MessageBuilder) => Rep`. I.e. it
uses a message builder to create a representation of an error
message. An example would be

```typescript
(l : MessageBuilder) => l.mustBe('a thingy!')
```

(The `'a thingy!` is hard-coded to english here. We can extend the
grammar of `MessageBuilder` to accommodate this. See
Customzing error messages)