Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/philipnilsson/bueno
Composable validators for forms, API:s in TypeScript
https://github.com/philipnilsson/bueno
Last synced: 1 day ago
JSON representation
Composable validators for forms, API:s in TypeScript
- Host: GitHub
- URL: https://github.com/philipnilsson/bueno
- Owner: philipnilsson
- Created: 2020-07-29T12:45:25.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2023-01-05T12:01:36.000Z (about 2 years ago)
- Last Synced: 2025-01-11T14:06:55.559Z (8 days ago)
- Language: TypeScript
- Homepage:
- Size: 1.32 MB
- Stars: 352
- Watchers: 8
- Forks: 7
- Open Issues: 13
-
Metadata Files:
- Readme: README.md
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
- Usage example: Vanilla JS form
- Usage example:
express
- Usage example:
react
+formik
- Customizing error messages
# 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)