Bueno
Composable validators for forms, API:s in TypeScript
Install / Use
/learn @philipnilsson/BuenoREADME
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 <a href="#api-documentation">API docs</a>
<ul> <li><a href="./src/docs/form.md">Usage example: Vanilla JS form</a></li> <li><a href="./src/docs/express.md">Usage example: <code>express</code></a></li> <li><a href="./src/docs/formik.md">Usage example: <code>react</code> + <code>formik</code></a></li> <li><a href="./src/docs/customizing-errors.md">Customizing error messages</a></li> </ul>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: 'philip@example.com',
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 <a href="#basic-schemas">basic
schemas</a> like number string, atLeast(10), exactly(null) and
by using <a href="#combinator-api">combinators</a> 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
<hr/>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
<a href="#apply">apply</a> • <a href="#both">both</a> • <a href="#compact">compact</a> • <a href="#defaultto">defaultTo</a> • <a href="#either">either</a> • <a href="#every">every</a> • <a href="#fix">fix</a> • <a href="#flip">flip</a> • <a href="#not">not</a> • <a href="#object">object</a> • <a href="#optional">optional</a> • <a href="#pipe">pipe</a> • <a href="#self">self</a> • <a href="#setmessage">setMessage</a> • <a href="#some">some</a> • <a href="#when">when</a>
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 <a href="#core">call signatures</a> 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 })
