bueno
v0.1.5
Published
Bueno - composable validators for forms, API:s and more.
Downloads
548
Readme
A tiny, composable validation library. Bueno primary aims to be an
improvement on form validation libraries like
yup
and
superstruct
, but
can also be used as a lightweight API validation library. You'll like
it if you need something
Try it out
You can check out bueno
directly in the browser in this
jsfiddle.
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:
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' }
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.
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 • checkPerKey • result
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
checkAsync :: <A>(value : A, schema : Schema<A, any>, locale : Locale) : string | null
Returns a string
with a validation error constructed using the given
locale, or null
if validation succeeded.
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
combinator)
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.
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.
both :: <A, B, C, D>(v : Schema<A, C>, w : Schema<B, D>,) => Schema_<A & B, C & D>
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.
either :: <A, B, C, D>(v : Schema<A, C>, w : Schema<B, D>,) => Schema_<A & B, C & D>
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
.
optional :: <A>(v : Schema<A, B>) : Schema<A | undefined, B | undefined>
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
not :: <A, B>(v : Schema<A, B>) => Schema_<A, B>
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".
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.
object :: <AS, BS>(vs :
{ [Key in keyof AS]: Schema<AS[Key], any> } &
{ [Key in keyof BS]: Schema<any, BS[Key]> }
) => Schema_<AS, BS>
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
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
.
compact :: <A, B>(p : Schema<A, B>) : Schema_<UndefinedOptional<A>, UndefinedOptional<B>> {
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.
fix :: <A, B = A>(fn : (v : Schema<A, B>) => Schema<A, B>) => Schema_<A, B>
TypeScript is not too great at inferring types using this combinators, so typically help it using an annotation as below
type BinTree<A> = {
left : BinTree<A> | null,
right : BinTree<A> | null,
value : A
}
const bintree = fix<BinTree<string>, BinTree<number>>(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.
type User = {
verified : boolean,
email : string | null
}
const schema = self<User, User>(user => {
return object({
verified: boolean,
email: user.verified ? email : exactly(null)
})
})
flip
Reverse a schema
flip :: <A, B>(schema : Schema<A, B>) => Schema_<B, A>
const schema = reverse(toNumber)
result(123, schema)
// '123'
defaultTo
Set a default value for a schema when it fails parsing.
defaultTo :: <A, B>(b: B, schema : Schema<A, B>) => Schema_<B, A>
const schema =
defaultTo(100, number)
result(null, schema)
// 100
pipe
Pipe the output of a schema as the input into another
pipe :: <A, B, C>(s : Schema<A, B>, t : Schema<B, C>) => Schema_<A, C>
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.
apply :: <A>(v : Schema<A, A>, value : A, path : string) => Schema_<any, A>;
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
.
every :: <A, B>(...vs : Schema<A, B>[]) => Schema_<A, B>
setMessage
Set the error message of a parser.
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
.
some :: <A, B>(...vs : Schema<A, B>[]) => Schema_<A, B>
when
"if-then-else" on parsers.
when :: <A, B>(cond : Schema<A, any>, consequent : Schema<A, B>, alternative : Schema<A, B>) => Schema_<A, B>
The "else" part is optional, in which case this combinator has the signature
when :: <A, B>(cond : Schema<A, any>, consequent : Schema<A, A>) => Schema_<A, A>
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
alphaNumeric :: Schema_<string, string>
Match an alphanumeric string.
check('acb123', alphaNumeric, enUS)
// null
check('acb|123', alphaNumeric, enUS)
// Must have letters and numbers only
any
any :: Schema_<any, any>
Successfully matches any input.
check(123, any, enUS)
// null
check(undefined, any, enUS)
// null
atLeast
atLeast :: <A>(lb : A) => Schema_<A, A>
Matches a value at least as big as the provided lower bound lb
.
const schema =
atLeast(100)
check(88, schema, enUS)
// 'Must be at least 100'
atMost
atMost :: <A>(ub : A) => Schema_<A, A>
Matches a value at most as big as the provided upper bound ub
.
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)
between :: (lb : number, ub : number) => Schema_<number, number>
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.
boolean :: Schema_<boolean, boolean>
date
Matches a Date
object.
date :: Schema_<Date, Date>
email
Matches an email (validated using a permissive regular expression)
email :: Schema_<string, string>
emptyString
Matches the empty string
emptyString :: Schema_<string, string>
even
Matches an even number
even :: Schema_<number, number>
exactly
Creates a schema that matches a single value, optionally using an equality comparison operator.
exactly :: <A>(target : A, equals : (x : A, y : A) => boolean = (x, y) => x === y) => Schema_<A, A>
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
id :: <A>() => Schema_<A, A>
The identity schema that always succeeds. Unlike any
, id
can be provided a type
argument other than the any
type.
const schema =
object({ foo: id<number>() })
check({ foo: 123 }, schema, enUS)
// null
check({ foo: 'hi!' }, schema, enUS)
// evaluates to null, but has a type error
integer
Match a whole number
integer :: Schema_<number, number>
length
Match an object with property length matching the schema argument
length :: <A extends { length : number }>(...vs : Schema<number, any>[]) => Schema_<A, A>
const username =
string(length(exactly(10)))
const items =
array(between(1, 10))
lessThan
Match a number less than the provided upper bound ub
lessThan :: (ub : number) => Schema_<number, number>
lift
Lift a function into a schema that uses the function for parsing.
const schema =
pipe(toNumber, lift(x => x + 1))
result('123', schema)
// 124
match
Match a string matching the provided regular expression.
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
moreThan :: (lb : number) => Schema_<number, number>
number
Match any number
number :: Schema_<number, number>
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
odd :: Schema_<number, number>
oneOf
Match exactly one of the given elements
const weekend =
oneOf('Fri', 'Sat', 'Sun'),
check('Sat', weekend, enUS)
// null
check('Wed', weekend, enUS)
// Must be Fri, Sat or Sun
swap
Swap elements
swap :: <A>(dict : [[A, A]]) => Schema_<A, A>
const optionalToEmptyString =
swap([[undefined, ''], [null, '']])
result(null, optionalToEmptyString)
// ''
result(undefined, optionalToEmptyString)
// ''
result('foo', optionalToEmptyString)
// 'foo'
optionalTo
Map null
and undefined
to another value.
optionalTo :: <A>(to : A) => Schema<null | undefined | A, A>;
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)
pair :: <A, B, C, D>(v : Schema<A, C>,w : Schema<B, D>) => Schema_<[A, B], [C, D]>
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.
path :: <A, B>(path : string, v : Schema<A, B>) => Schema<A, B>
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.
size :: <A extends { size : number }>(...vs : Schema<number, any>[]) => Schema_<A, A>
string
Match a string
string :: Schema_<string, string>
sum
Match an array with sum matching the schema argument.
sum :: (...vs : Schema<number, any>[]) => Schema_<number[], number[]>
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 instead.
toDate :: Schema_<string, Date>
toJSON
Converts a string to JSON.
toJSON :: Schema_<string, any>
toNumber
Converts a string to a number.
toNumber :: Schema_<string, number>
toString
Converts a value to a string.
toString :: Schema_<string, string>
toURL
Converts a string to an 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.
array :: <A, B>(v : Schema<A, B>,) : Schema_<A[], B[]>
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.
iterable :: <A>() => Schema_<Iterable<A>, Iterable<A>>
const schema =
iterable<string>()
check(['hello', 'world'], schema, enUS)
// null
map
Create a schema that matches a Map
from schemas describing the keys
and values respectively.
map :: <A, B, C, D>(k : Schema<A, C>, v : Schema<B, D>) => Schema_<Map<A, B>, Map<C, D>>
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.
set :: <A, B>(v : Schema<A, B>) => Schema_<Set<A>, Set<B>>
const schema =
set(any)
check(new Set([1, 'a', true], schema, enUS)
// null
check([1, 'a', true], schema, enUS)
// 'Must be a set'
const schema =
set(toNumber)
parse(new Set(['1', '2', '3']))
// Set(3) { 1, 2, 3 }
toArray
Convert an iterable to an array.
toArray :: <A>() => Schema_<Iterable<A>, A[]>
toMap
Convert an iterable of pairs to a Map
toMap :: <A, B>() => Schema_<Iterable<[A, B]>, Map<A, B>>
toMapFromObject
Convert an objet into a Map.
toMapFromObject :: <A extends symbol | string | number, B>() : Schema_<{ [key in A]: B }, Map<A, B>>
result({ 'a': 3, 'b': 10, 'c': 9 }, toMapFromObject())
// Map(3) { 'a' => 3, 'b' => 10, 'c' => 9 }
It only works on "real" objects.
check('', toMapFromObject(), enUS)
// 'Must be an object'
toSet
Convert an iterable of values into a set.
toSet :: <A>() : Schema_<Iterable<A>, Set<A>>
Factory functions
Factory functions let you create new schema definitions.
createSchema
createSchema :: <A, B>(
parse : SchemaFactory<A, B>,
unparse : SchemaFactory<B, A> = irreversible('createSchema')
) : Schema_<A, B>
Create a schema from two "parser factories" for each "direction" of parsing. (See Bidirectionality.) A single factory may be provided, but the schema will not be invertible.
A SchemaFactory
is simply a function of type
type SchemaFactory<A, B> = (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.
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<A, B>
The type of a schema. Converts a value of type A
into one of type B
and validates the result.
Schema_<A, B>
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.
const schema =
number(even, atLeast(12))
Action
An Action<A>
is either an A
or a Promise<A>
. 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
Message
This type is used to create error messages that are independent of a specific locale.
It is a value of type <Rep>(l : MessageBuilder<Rep>) => Rep
. I.e. it
uses a message builder to create a representation of an error
message. An example would be
<Rep>(l : MessageBuilder<Rep>) => 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)