folktale-validations
v2.12.1
Published
[![NPM Version](https://img.shields.io/npm/v/folktale-validations.svg)](https://www.npmjs.com/package/folktale-validations) [![codecov](https://img.shields.io/codecov/c/github/Undistraction/folktale-validations.svg)](https://codecov.io/gh/Undistraction/fo
Downloads
49
Maintainers
Readme
folktale-validations
A library of validators using folktale's Validation
including utility functions for combining and composing validations, and constraint-based validation, allowing you to validate objects or object graphs. Includes easily customisable error message rendering, and is easily extended with your own validations.
Much of the basic validation relies on well tested predicates provided by ramda-adjunct.
The library is well tested (on Node 7, 8 and 9) and the validators you'll create yourself are easy to test.
Structure
The project is broken into:
- validators: Validators that work on a single value or a collection of values.
- helpers: Functions helping you create, combine or change the behaviour of validators.
- constraints: A system for validating whole objects or object graphs, with validation and transformation of values.
- failures: Rendering of failed validations to human-readable error messages.
Install
yarn add folktale-validations
or
npm install folktale-validations
Validators
Many validators are included, but it is very easy to create your own. The following are all ready to use out of the box.
Predicates
Basic Types
- validateIsArray
- validateIsNotArray
- validateIsObject
- validateIsNotObject
- validateIsBoolean
- validateIsNotBoolean
- validateIsString
- validateIsNotString
- validateIsFunction
- validateIsNotFunction
- validateIsNumber
- validateIsNotNumber
Complex Objects
- validateIsDate
- validateIsNotDate
- validateIsRegExp
- validateIsNotRegExp
- validateIsPlainObject
- validateIsNotPlainObject
Nil Values
- validateIsNaN
- validateIsNotNaN
- validateIsNil
- validateIsNull
- validateIsNotNull
- validateIsUndefined
- validateIsNotUndefined
Emptyness
- validateIsEmpty
- validateIsNotEmpty
- validateIsEmptyArray
- validateIsNonEmptyArray
- validateIsEmptyString
- validateIsNonEmptyString
Validity
- validateIsValidNumber
- validateIsNotValidNumber
- validateIsValidDate
- validateIsNotValidDate
Numericality
- validateIsInteger
- validateIsNotInteger
- validateIsPositive
- validateIsNegative
- validateIsNonPositive
- validateIsNonNegative
Truth
- validateITrue
- validateIsFalse
- validateIsTruthy
- validateIsFalsy
Association
- validateIsLengthGreaterThan
- validateIsLengthLessThan
- validateIsLengthBetween
Array
- validateArrayElements - Validate all elements with supplied validator
- validateIsArrayOf - Validate is array and validate all elements with supplied validator
Object
- validateObjectValues - Validate values using a map of key-validator pairs
- validateRequiredKeys - Required keys must be present
- validateWhitelistedKeys - Keys must appear on whitelist
- validateExclusiveKeys - Only one key must appear from the supplied list
Other
- validateIsWhitelistedValue - Value must appear on whitelist
- validateIsNotBlacklistedValue - Value must not appear on blacklist
- validateIsNumberWithUnit - Value must be a number followed by supplied unit
- validateIsValidNonNegativeNumberWithUnit
- validateIsValidPositiveNumberWithUnit
Constraints
- validateObjectWithConstraints - Validate an object or object graph using a constraint object that describes a valid object
Helpers
- allOfValidator - All validations must pass (will always run all validations)
- andValidator - Both validations must pass (will always run both validations)
- anyOfValidtor - Any validations must pass (will always run all validations)
- orValidator - Either validation must pass (will always run both validations)
- untilFailureValidator - All validations must pass (short-circuits on failure)
- predicateValidator - Create a validator using a simple predicate
- regExpValidator - Create a validator that uses a RegExp for validation
Usage
Introduction
Note: All examples in this introduction can be run as working tests in: src/__tests__/docs/readme.js
and can be run using:
yarn run test:readme
or
npm run test:readme
Every validation you perform will return a Validation object which will be either a Failure
or a Success
. There are isFailure
and isSuccess
functions exported from index.js
to help you check, however you can also use matchWith
and a variety of other methods to handle both cases which are outlined in the link above.
- If it is a
Success
, the object'svalue
will contain the validated value. - If it is a
Failure
, the object'svalue
will contain a payload describing the failure.
A payload is a simple object describing the failure and has three fields:
uid
- the UID of the validatorvalue
- the value that failed validationargs
- an array of values relating to the failure, for examplevalidateIsWhiteListedValue
will include two arrays - one of all whitelisted values, and one of values discovered that weren't included in the whitelist.
This information can be rendered into a human-readable message using a Failure Renderer. The library ships with a preconfigured renderer which knows how to render failures from all included validators, including complex nested failures. You can supply your own messages for some or all of these validators, as outlined below. Outputting payload objects from the validators makes them easy to test.
Many of the validators are simple predicate validators - supply them with a value and they will either succeed or fail:
Example 1 - Predicate Validator
const validValue = `a`
const successfulValidation = validateIsString(validValue)
expect(isSuccess(successfulValidation)).toBeTrue()
expect(successfulValidation.value).toEqual(validValue)
const invalidValue = 1
const failedValidation = validateIsString(invalidValue)
const message = failureRenderer(failedValidation.value)
expect(isFailure(failedValidation)).toBeTrue()
expect(failedValidation.value).toEqual({
uid: `folktale-validations.validateIsString`,
value: invalidValue,
args: [],
})
expect(message).toEqual(`Wasn't String`)
Other validators require configuring before use.
Example 2 - Association Validator
const configuredValidator = validateIsLengthGreaterThan(2)
const validValue = `abc`
const successfulValidation = configuredValidator(validValue)
expect(isSuccess(successfulValidation)).toBeTrue()
expect(successfulValidation.value).toEqual(validValue)
const invalidValue = `a`
const failedValidation = configuredValidator(invalidValue)
const message = failureRenderer(failedValidation.value)
expect(isFailure(failedValidation)).toBeTrue()
expect(failedValidation.value).toEqual({
uid: `folktale-validations.validateIsLengthGreaterThan`,
value: invalidValue,
args: [2],
})
expect(message).toEqual(`Length wasn't greater than '2'`)
Validators can also validate Objects - either keys or values. In the following example, the values of an object are validated, using a different validator for each key.
Example 3 - Object Validator
const configuredValidator = validateObjectValues({
a: validateIsNumber,
b: validateIsString,
c: validateIsNotEmpty,
})
const validValue = {
a: 1,
b: `example`,
c: [1, 2, 3],
}
const successfulValidation = configuredValidator(validValue)
expect(isSuccess(successfulValidation)).toBeTrue()
expect(successfulValidation.value).toEqual(validValue)
const invalidValue = {
a: `example`,
b: true,
c: [],
}
const failedValidation = configuredValidator(invalidValue)
const message = failureRenderer(failedValidation.value)
expect(isFailure(failedValidation)).toBeTrue()
expect(failedValidation.value).toEqual({
fields: {
a: {
uid: `folktale-validations.validateIsNumber`,
value: `example`,
args: [],
},
b: {
uid: `folktale-validations.validateIsString`,
value: true,
args: [],
},
c: {
uid: `folktale-validations.validateIsNotEmpty`,
value: [],
args: [],
},
},
})
expect(message).toEqualMultiline(`
Object
included invalid value(s)
– a: Wasn't Number
– b: Wasn't String
– c: Was Empty`)
An Array of values can also be validated, using a single validator for all the values in the array.
Example 4 - Array Validator
const configuredValidator = validateArrayElements(validateIsRegExp)
const validValue = [/a/, /b/, /c/]
const successfulValidation = configuredValidator(validValue)
expect(isSuccess(successfulValidation)).toBeTrue()
expect(successfulValidation.value).toEqual(validValue)
const invalidValue = [/a/, `/b/`, /c/]
const failedValidation = configuredValidator(invalidValue)
const message = failureRenderer(failedValidation.value)
expect(isFailure(failedValidation)).toBeTrue()
expect(failedValidation.value).toEqual({
children: {
'1': {
uid: `folktale-validations.validateIsRegExp`,
value: `/b/`,
args: [],
},
},
})
expect(message).toEqualMultiline(`
Array included invalid value(s)
– [1] Wasn't RegExp`)
Combining Validations
The library offers a number of helper functions for combining or composing validations. In the following example, allOfValidator
is used to compose two validations into a single validation:
Example 5 - Composed Validations
const configuredValidator = allOfValidator([
validateIsString,
validateIsLengthLessThan(5),
])
const validValue = `abcd`
const successfulValidation = configuredValidator(validValue)
expect(isSuccess(successfulValidation)).toBeTrue()
expect(successfulValidation.value).toEqual(validValue)
const invalidValue = 1
const failedValidation = configuredValidator(invalidValue)
const message = failureRenderer(failedValidation.value)
expect(isFailure(failedValidation)).toBeTrue()
expect(failedValidation.value).toEqual({
and: [
{
uid: `folktale-validations.validateIsString`,
value: 1,
args: [],
},
{
uid: `folktale-validations.validateIsLengthLessThan`,
value: 1,
args: [5],
},
],
})
expect(message).toEqual(`Wasn't String and Length wasn't less than '5'`)
These validations can themselves be composed, for example:
Example 6 - Nested Composed Validations
const configuredValidator = allOfValidator([
orValidator(validateIsString, validateIsNumber),
validateIsLengthLessThan(5),
])
const validValue = `abcd`
const successfulValidation = configuredValidator(validValue)
expect(isSuccess(successfulValidation)).toBeTrue()
expect(successfulValidation.value).toEqual(validValue)
const invalidValue = null
const failedValidation = configuredValidator(invalidValue)
const message = failureRenderer(failedValidation.value)
expect(isFailure(failedValidation)).toBeTrue()
expect(failedValidation.value).toEqual({
and: [
{
or: [
{
uid: `folktale-validations.validateIsString`,
value: null,
args: [],
},
{
uid: `folktale-validations.validateIsNumber`,
value: null,
args: [],
},
],
},
{
uid: `folktale-validations.validateIsLengthLessThan`,
value: null,
args: [5],
},
],
})
expect(message).toEqual(
`(Wasn't String or Wasn't Number) and Length wasn't less than '5'`
)
Constraint-based validations
Using constraints allows you to describe what constitutes a valid object or nested object graph. It also allows you to tansform the received values and apply default values for missing props. This involves three steps:
- Create a constraint object
- Configure
validateObjectWithConstraints
with a constraint object - Validate an object
Note: As part of the validation process, the constraints object itself is validated, and you will recieve a Failed Validation explaining where the problem is if it is invalid.
Note: Take a look at src/constraints/constraints.js
to see the constraints object that is used to validate constraints objects supplied to validateObjectWithConstraints()
.
Example 7 - Constraints With Flat Object
it(`returns expected values`, () => {
const constraints = {
fields: [
{
name: `a`,
validator: validateIsString,
},
{
name: `b`,
validator: validateIsArrayOf(validateIsNumber),
},
{
name: `c`,
validator: validateIsDate,
},
],
}
const confguredValidator = validateObjectWithConstraints(constraints)
const validValue = {
a: `abc`,
b: [1, 2, 3],
c: new Date(`01-01-2001`),
}
const successfulValidation = confguredValidator(validValue)
expect(isSuccess(successfulValidation)).toBeTrue()
expect(successfulValidation.value).toEqual(validValue)
const invalidValue = {
a: 123,
b: null,
c: `01-01-2001`,
}
const failedValidation = confguredValidator(invalidValue)
const message = failureRenderer(failedValidation.value)
expect(isFailure(failedValidation)).toBeTrue()
expect(failedValidation.value).toEqual({
fields: {
a: {
uid: `folktale-validations.validateIsString`,
value: 123,
args: [],
},
b: {
uid: `folktale-validations.validateIsArray`,
value: null,
args: [],
},
c: {
uid: `folktale-validations.validateIsDate`,
value: `01-01-2001`,
args: [],
},
},
})
expect(message).toEqualMultiline(`
Object
included invalid value(s)
– a: Wasn't String
– b: Wasn't Array
– c: Wasn't Date`)
})
There are a number of other valid attributes for the constraints object.
Validating the keys of the object
By default all object's are validated by two field validators - one that checks there are no keys present that aren't described by the constraitns, and one that checks that any required keys (see below) are present. You can use the fieldsValidator
attribute to supply an additional validator for the object's keys themselves. For example you might want to check that only one of a set of keys should appear per object.
If you want to allow arbitray keys on your object in addition to the keys you are validating you can use set the whistelistKeys
key on the constraint for that object. This will prevent any errors being thrown if a key is discovered thay is not defined in the constraints. This allows you to only validate a subset of an object's keys or use object maps that are entirely composed of arbitrary keys.
Object values
isRequired
If a key must be present, you can add an isRequired
attribute to the constraints object for that field. This will cause validation to fail if the key does not appear on the object. Note: your constraints will be invalid if you use isRequired
and defaultValue
on the same field.
defaultValue
If you want to supply a defualt value for a key if it isn't present you can add a defaultValue
attribute. This add a key with the default value to the object when it is validated. Note: Nothing is mutated - the value returned will be a copy of the object with the key/value pair added.
transformer
If you want to transform a supplied value that has passed validation, you can supply a transformer function as a field's transformer
attribute. Note: the transformer will be applied after any defaultValue
. The transformer function should be unary and return the transformed value.
Example 8 - Using isRequired, defaultValue and transformer
const constraints = {
fields: [
{
name: `a`,
isRequired: true,
validator: validateIsString,
transformer: toUpper,
},
{
name: `b`,
validator: validateIsBoolean,
defaultValue: true,
},
],
}
const confguredValidator = validateObjectWithConstraints(constraints)
const validValue = {
a: `abc`,
}
const expectedValue = {
a: `ABC`,
b: true,
}
const successfulValidation = confguredValidator(validValue)
expect(isSuccess(successfulValidation)).toBeTrue()
expect(successfulValidation.value).toEqual(expectedValue)
const invalidValue = {
b: false,
}
const failedValidation = confguredValidator(invalidValue)
const message = failureRenderer(failedValidation.value)
expect(isFailure(failedValidation)).toBeTrue()
expect(failedValidation.value).toEqual({
fieldsFailureMessage: {
uid: `folktale-validations.validateRequiredKeys`,
value: { b: false },
args: [[`a`], [`a`]],
},
})
expect(message).toEqual(`Object missing required key(s): ['a']`)
Validating Object Graphs
You aren't limitted to flat objects. You can use full object graphs comprising of objects and arrays. To describe these graphs you use two additional attributes of the constraints object: children
and value
. In the following example a complex object graph is validated. Note: your constraints will be invalid if you use children
and value
on the same field.
Example 9 - Constraints With Object Graph
const constraints = {
fields: [
{
name: `a`,
isRequired: true,
validator: validateIsObject,
value: {
fields: [
{
name: `a-a`,
isRequired: true,
validator: validateIsBoolean,
},
{
name: `a-b`,
isRequired: true,
validator: validateIsNonEmptyArray,
children: {
fields: [
{
name: `a-b-a`,
isRequired: true,
validator: validateIsString,
},
],
},
},
],
},
},
],
}
const confguredValidator = validateObjectWithConstraints(constraints)
const validValue = {
a: {
[`a-a`]: true,
[`a-b`]: [
{
[`a-b-a`]: `abc`,
},
{
[`a-b-a`]: `def`,
},
],
},
}
const successfulValidation = confguredValidator(validValue)
expect(isSuccess(successfulValidation)).toBeTrue()
expect(successfulValidation.value).toEqual(validValue)
const invalidValue = {
a: {
[`a-a`]: true,
[`a-b`]: [
{
[`a-b-a`]: `abc`,
},
{
[`a-b-a`]: 123,
},
],
},
}
const failedValidation = confguredValidator(invalidValue)
const message = failureRenderer(failedValidation.value)
expect(isFailure(failedValidation)).toBeTrue()
expect(failedValidation.value).toEqual({
fields: {
a: {
fields: {
'a-b': {
children: {
'1': {
fields: {
'a-b-a': {
uid: `folktale-validations.validateIsString`,
value: 123,
args: [],
},
},
},
},
},
},
},
},
})
expect(message).toEqualMultiline(`
Object included invalid value(s)
– a: Object included invalid value(s)
– a-b: Array included invalid value(s)
– [1] Object included invalid value(s)
– a-b-a: Wasn't String`
)
})
Customising Existing Validators
Replacing Existing Messages
The messages ouput during rendering can be configured by passing a map of functions via a configuration object to configureValidators()
. Here you can override the validator messages by using one of the existing keys and you can add your own key/function pairs rendering failures from your own validators. The default validator messages can be found here: src/config/customise/validatorMessagesDefaults.js
.
Each key should map to a function that returns a formatted message for that validator. The uid
of the payload returned from a validator will be used to locate the appropriate function, which will then have the values in the payload's args
applied to it. It is recommended you use some kind of namespaced uid. This library uses uids like this: folktale-validations.validateIsArray
.
Example 10 - Customising Validation Failure Messages
it(`returns expected values`, () => {
const newMessage = `Boolean it isn't`
const { failureRenderer: configuredFailureRenderer } = configureRenderers({
validatorMessages: {
[validatorUids.VALIDATE_IS_BOOLEAN]: always(newMessage),
},
})
const failedValidation = validateIsBoolean(`yoda`)
const message = configuredFailureRenderer(failedValidation.value)
expect(message).toEqual(newMessage)
})
Creating Validator Based On Existing Validator
The simplest way to customise an existing valiadator is simply to configure it as we have done in previous examples. You can then export the configured validator for use throughout your application. However if you want to add your own message that is specific to the configured validator you can decorate the validator, supplying it with a new uid.
Example 11 - Creating Validator Based On Existing Validator
const newUID = `example.validateIsValidTitle`
const newMessageFunction = whitelist => `Wasn't a title: ${whitelist}`
const titles = [`mr`, `mrs`, `miss`, `ms`, `dr`, `mx`]
const { failureRenderer: configuredFailureRenderer } = configureRenderers({
validatorMessages: {
[newUID]: newMessageFunction,
},
})
const validateIsValidTitle = compose(
decorateValidator(newUID),
validateIsWhitelistedValue
)(titles)
const failedValidation = validateIsValidTitle(`emperor`)
const message = configuredFailureRenderer(failedValidation.value)
expect(message).toEqual(`Wasn't a title: mr,mrs,miss,ms,dr,mx`)
Customising Constraint Validation
You can also customise the rendering of constraint-based validation failures. In this instance things are more complicated as there is significant formatting as well as text rendering. Again, you can pass in an object via the configuration object of configureValdidators()
. The default helpers can be found here: src/config/customise/failureRendererHelpersDefaults.js
.
Example 12 - Customising Constraint-based Validation Formatting
Coming Soon
Adding Your Own Validators
Using existing helpers
There are a couple of helpers offered to use as a basis for your own validators - predicateValidator
and regExpValidator
. In the following example we'll create a validator that checks that a string doesn't have any whitespace.
Example 13 - Creating a Validator Using Helpers
const UID = `example.validateHasNoWhitespace`
const newMessageFunction = always(`Should not contain whitespace`)
const regExp = /^\S+$/
const { failureRenderer: configuredFailureRenderer } = configureRenderers({
validatorMessages: {
[UID]: newMessageFunction,
},
})
const validateHasNoWhitespace = regExpValidator(UID, regExp)
const successfulValidation = validateHasNoWhitespace(`ab`)
expect(isSuccess(successfulValidation)).toBeTrue()
const failedValidation = validateHasNoWhitespace(`a b`)
const message = configuredFailureRenderer(failedValidation.value)
expect(message).toEqual(`Should not contain whitespace`)
From scratch
The validator interface is very simple. They should:
- Accept any configuration values first
- Accept the data to validate last
- Should be curried
- If the validation is succeeds they should return a
Success
with itsvalue
set to the validated data. - If the validation fails they shoudl return a
Failure
with itsvalue
set to a payload.
Payloads
A payload is created using toPayload()
which takes three arguments:
- A UID for that validator
- The value that was validated
- (optional) an array of values to be supplied to the function that will render the message for this validator.
Example 14 - Creating a Validator From Scratch
const UID = `example.validateContainsChars`
const newMessageFunction = chars => `Didn't contain chars: [${chars}]`
const { failureRenderer: configuredFailureRenderer } = configureRenderers({
validatorMessages: {
[UID]: newMessageFunction,
},
})
const containsChars = (chars, s) =>
compose(isEmpty, reject(flip(contains)(s)))(chars)
const { Success, Failure } = Validation
const validateContainsChars = curry(
(chars, value) =>
containsChars(chars, value)
? Success(value)
: compose(Failure, toPayload)(UID, value, [chars])
)
const configuredValidator = validateContainsChars([`a`, `b`, `c`])
const successfulValidation = configuredValidator(`cab`)
expect(isSuccess(successfulValidation)).toBeTrue()
const failedValidation = configuredValidator(`cat`)
const message = configuredFailureRenderer(failedValidation.value)
expect(message).toEqual(`Didn't contain chars: [a,b,c]`)
Arguments Failure Renderer
If you want to output a message relating to arguments instead of an object, you can use the argumentsFailureRenderer
which will render a more appropriate message. It is also available from the object returned by defaultValidators()
.
NPM Module
The NPM module includes:
lib
directory with imports transpiled for use by bundlers.
Contributing
I welcome contributions, but ask you open an issue to discuss the bugfix or feature before you open a PR. Please keep any PRs as focussed as possible.
Maintainance
Tests
Tests are written with Jest.
Run tests in watch mode
yarn test
Run tests once
yarn run test:noWatch
View HTML Coverage report
yarn run test:cov
Build
yarn run build
Publish to NPM
yarn run publish:patch
Or
yarn run publish:minor
Or
yarn run publish:major
Developing
Adding Predicates
Simple predicates and their tests are generated from a series of consts and data files.
- Add the predicate name to
src/const/predicateNames.js
- Add predicate and negation to
src/const/validatorUids.js
- Add predicate and negation to
src/validators/predicate/predicates.js
- Add predicate and negation export to
src/validators/predicate/generatedPredicateValidators.js
- Add predicate and negation data to
src/__tests__/testHelpers/data/predicateValidators.js
- Add default message to
src/config/defaults/validatorMessagesDefaults.js