validationator
v2.0.4
Published
A versitile data validator and static type checker. Creat complex models for deeply nested data structures. Create statically typed functions. Switch to bool mode of use in form validation, and much more.
Downloads
9
Maintainers
Readme
Validationator is a flexible and powerful javascript typing & data validation library. Validationator has four exports:
- validations - an object of validation methods. These are used to power all other functions of the library. They can be extended, added to, and used ad hoc.
- validate - the core of the validationator library. Define a validation model of your data and validate to either return the original value or throw a useful error. Use warn or bool mode to override this behavior.
- type - a wrapper around the validate core library that allows for an alternative more traditional typed language syntax.
- validateFunc - define the input and output models of a function much like react PropTypes. Create statically typed functions. Uses the validate core.
- typeFunc - pass in a function and validation model to return a reusable typed function
- typeClass - create fully typed classes by defining a class model
- TypeClass - allows for use of setProps in classes for internal static typing.
- TypedVal - wrap values in geter and setter function that consistently enforce validation
Why use validationator:
- statically type js functions for more robust applications
- it's super small, super fast, and backward compatible
- throw useful errors for faster debugging
- very versatile API
- validate forms with ease
- easily add your own custom validations
Wait, doesn't flow do this already? Yes, flow does allow for static typing, but it does not allow for nested data structures or non-type validation checks. This is also a semi-runtime library where flow is compile-time. I say semi because the library is set to look for a 'production' node environment variable and will to bypass any validation. This behavior can be easily overridden.
The default behavior of validate is to return the original value on success and to throw an error on failure. This can be configured to 'bool' and 'warn' mode as well.
Getting Started
// ==> npm i -S validationator
// for front end
import { validate, validations, validateFunc, type } from 'validationator'
// or for Node
const { validate, validations, validateFunc, type } = require('validationator')
validate('[email protected]', String)
Overview
validations
A validation is simply a method that takes in a value and an options object, and then throws errors if certain criteria are met. They are very fast, throw useful errors, but do not have any of the options or syntactic sugar added by the validate core.
validations.number(12, {
decimails: 0,
max: 20
})
validate
Takes in a value and a validation model. By default returns the original value on success and throws a useful error on failure. There are various options to configure this behavior, add and extend validations, and much more. More API details available below.
// do very simple data type checks
validate('asdf', String)
// add extra options
validate('asdf', {
String,
minLen: 2
})
// use various non data type validations
validate('[email protected]', 'email')
type
An alternative syntax for the validate core. Define the data type first and options after. Define a secondary data type and validate with same options object (the options object inherits the options of the data type).
const homepage = 'www.hattonpoint.com'
type(String, homepage)
type(String, homepage, {
maxLen: 100
})
const x = type // read 'check'
const validatedUrl = x(String, homepage, {
type: 'url',
maxLen: 100
})
validateFunc
Syntactic sugar that makes it easier to create statically typed functions. Define an inputModel and outputModel to functions.
const increment = num => (num + 1)
increment.inputModel = Number
increment.outputModel = Number
validateFunc(increment, 3)
// There are various ways to create statically typed
// functions and classes discussed in sections below.
typeFunc
pass in a function and validation model to return a reuseable typed function
const increment = num => (num + 1)
const typedIncrement = typeFunc(increment, {
inputModel: Number,
outputModel: Number
})
typedIncrement(1) // ==> 2
typedIncrement('asdf') // throws error
typeClass
Using the fundamental components of validate and validateFunc, typeClass allows you to create fully typed js classes be defining a separate class validation model. Instance construction and prop changes and protected by validation checks. Methods are typed using validateFunc. This gives you many of the benefits of static typing without having to clutter up your original code.
let Hooligan = class Hooligan {
constructor ({ name, height }) {
this.name = name
this.height = height
this.stomach = []
this.mood = 'grumpy'
}
eat (food) {
this.stomach = [ ...this.stomach, food ]
return 'uhh.. not food'
}
drink (drank) {
if (drank === 'coffee') this.mood = 'better'
}
}
Hooligan.model = {
constructor: {
name: String,
height: Number
},
props: {
name: String,
stomach: { Array, allChildren: String },
mood: String,
height: Number
},
methods: {
eat: {
inputModel: String,
outputModel: String
},
drink: {
inputModel: String,
outputModel: undefined
}
}
}
Hooligan = typeClass(Hooligan)
// The parameters passed into the constructor are typechecked
const hooligan = new Hooligan(12) // ==> throws error
const Ian = new Hooligan({ name: 'Ian', height: 60 })
// After converting your class to a typed class, call your props
// as functions. This is your getter method. Getter methods do
// not do any validation since the setter and constructor validation
// should ensure it is always the correct type.
Ian.name() // ==> 'Ian'
Ian.name // ==> undefined
// typeClass creates camelCased setter methods for your defined
// props. These methods take in a new value and validate it
// before setting the class prop value. The setter methods return
// the new value of the prop, or throw and error.
Ian.setName('Ignatius') // ==> 'Ignatius'
Ian.name() // ==> 'Ignatius'
Ian.setName(123) // throws error
// The inputs and outputs of defined methods are wrapped using the
// validateFunc function.
const Ian = new Hooligan({ name: 'Ian', height: 60 })
Ian.mood() // ==> 'grumpy'
Ian.drink('coffee')
Ian.mood() // ==> 'better'
Ian.drink(123) // throws error
TypedClass
One of the weaknesses of typeClass is that still allows methods to set the props of a class internally without validation. TypedClass was introduced to address this issue. Extend your classes from TypedClass and pass constructor props directly into super to gain access to setProps in your class methods. setProps operates much like setState in react. Pass in an object with the keys of the props you want to change with updated values. Usine setProps these values are validated whenever you attempt to update them.
const Hooligan = class Hooligan extends TypedClass { // updated
constructor ({ name, height }) { // updated
super({ name, height })
this.name = name
this.height = height
this.stomach = []
this.mood = 'grumpy'
}
eat (food) {
this.setProps({ // updated
stomach: [ ...this.stomach, food ]
})
return 'uhh.. not food'
}
drink (drank) {
if (drank === 'coffee') this.mood = 'better'
}
}
Hooligan.model = hooliganModel
TypedVal
Wrap values in geter and setter methods that consistently enforce validation.
const myEmail = '[email protected]'
const typedEmail = new TypedVal(myEmail, { type: 'email' })
// values are checked on construction
const notEmail = 'hattonpoint.com'
new TypedVal(notEmail, { type: 'email' }) // throws error
// new values are accessed with get and set methods
typedEmail.get() // ==> myEmail
// use g and s shorthand methods
typedEmail.g() // ==> myEmail
const newEmail = '[email protected]'
typedEmail.set(newEmail) // newEamil
typedEmail.g() // ==> newEmail
typedEmail.set(notEmail) // throws error
typedEmail.s(myEmail) // ==> myEmail
typedEmail.g() // ==> myEmail
API Details
validate.js
// do very simple data type checks
validate('asdf', String)
// this is all valid syntax too
validate('asdf', 'string')
validate('asdf', { type: 'string' })
validate('asdf', { type: String })
// add extra options
validate('asdf', {
String,
minLen: 2
})
// use various non data type validations
validate('[email protected]', 'email')
// validate children of arrays and objects
const hooligans = [ 'Mike', 'Erik', 'Kenny' ]
validate(hooligans, { Array, allChildren: String })
// validate deeply nested data structures
const backPack = {
brand: 'North Face',
price: 20,
contents: [
'water bottle',
'jacket'
],
fannyPack: {
style: -1000,
contents: 'questionable'
}
}
const validationModel = {
Object,
brand: String,
price: { Number, max: 50 },
contents: { Array, allChilden: String },
fannyPack: {
Object,
children: {
// pass multiple validations in an array for or checks
style: [{ Number, max: 0 }, String, Object],
contents: { type: 'is', exactly: 'questionable' }
}
}
}
validate(backPack, validationModel)
// you can even validate classes
const Hooligan = class Hooligan {
constructor (name) {
this.name = name
this.stomach = []
this.mood = 'grumpy'
}
eat (food) {
this.stomach.push(food)
}
drink (drank) {
if (drank === 'coffee') this.mood = 'better'
}
}
const Ian = new Hooligan('Ian')
validate(Ian, {
type: 'instance',
of: Hooligan,
strict: true,
children: {
name: String,
eat: Function,
drink: Function
}
})
Validation Model API
A validation model is an object that describes your value. It can be a simple type check or a complex data evaluation with appended tests.
'type' is the only required validation. Each type will also have its own set of custom validation options. There are many validation types available, many powered by the validator.js library. See the validation type section below for more details.
The default behavior of validate is to return the original value on success and throw an error on failure. This behavior can be changed using the various options outlined below.
By default, all validated values are required and any falsy value will fail the required check. See the acceptedNulls and notRequired options below to change this behavior.
validate and validateFunc both default to bypassing all validation when a 'production' node environment is detected. (ie. process.enc.NODE_ENV === 'production'). This is so typing errors can be found in development but would prevent the performance dip from validation in production. This behavior can be controlled with the ON and OFF options.
shared options:
Some options are shared by all validation types. They are:
- name<string> - name to be used in error reporting (optional)
validate('hello', { String, name: 'hello-string' })
- on<boolean> - if true the validate function will ignore the check for a production node environment and run anyway. This is an override option to let the validation run in production.
process.env.NODE_ENV = 'production'
validate(12, { Number, on: true })
// validates even in production
// off, warn, and bool API just like on.
- off<boolean> - if true turns off all validation. Same as what happens in production environment
- warn<boolean> - if true errors delivered through console.warn rather than throwing an error. This allows for non-blocking validation.
- bool<boolean>- if true validate will return true or false to indicate validation rather than throw errors.
- notRequired<boolean>: all fields with a model are considered required unless this is true.
- acceptedNulls<array>: by default, every falsey value is considered missing when doing the default required field check. This includes values like false and 0. In many instances, you may consider falsey values such as these valid. Pass all accepted null values in an array to override.
// on, off, warn, and bool can all also be
// set at the function level like this:
validate.bool = true
// every instance of validate from this
// point on will have the bool option activated.
validate(0, Number) // ==> false
// (because all values are requred by default and
// all falsy values fail the required check by default)
validate(1, Number) // ==> true
validate(0, { Number, notRequired: true }) // ==> true
validate(undefined, { Number, notRequired: true }) // ==> true
const acceptedNulls = [0]
validate(0, { Number, acceptedNulls }) // ==> true
validate(undefined, { Number, acceptedNulls }) // ==> false
- extend<cb[value, validation> extend is a callback function that gives you access to variables of the current evaluation so you can inject custom rules. See below:
validate({ test: 1 },
{ Object,
extend: value => {
if (value.test === 1) throw new Error('The value is 1! It is not supposed to be 1!!!')
},
}
)
boolean
Just a type check.
validate(true, Boolean) // ==> true
validate('asdf', Boolean) // throws error
validate(false, { Boolean, acceptedNulls: [false] }) // ==> false
// (because it is returning the original value on success)
validate(false, {
Boolean,
bool: true,
acceptedNulls: [false]
})
// ==> true
// But really though,
// think about whether or not you need to use validate
typeof false === 'boolean' // ==> true
// same thing, way faster.
Remember to add false to the acceptedNulls list if you want them not to fail this validation
function
Just a type check.
const increment = int => (int + 1)
validate(increment, 'function') // ==> increment
string
- maxLength<number>
- minLength<number>
- regEx<regEx> - tests the string against a provided regEx expression
- includes<string> - fail validation if string does not include string
- notIncludes<string> - fail validation if string does include string
- includesAny<string> - fail validation if string does not include any of the strings in an array
- notIncludesAny<[string]> - fail validation if string does include any of the strings in an array
- upperCase<bool> - fail validation if string is not all uppercase
- lowerCase<bool> - fail validation if string is not all lowercase
number
- max<number>
- min<number>
- decimals<number> - specifies the maximum amount of decimals. Less than the number provided will pass.
- regEx<regEx> - tests the string against a provided regEx expression
object
- minLength<number>
- maxLength<number>
- requredKeys<array>
- includes<any> - fail validation if object does not include value
- notIncludes<any> - fail validation if object does include value
- includesAny<any> - fail validation if object does not include any of the values in an array
- notIncludesAny<[any]> - fail validation if object does include any of the values in an array
- allChildren<validationModel> - validates every child against the given validation model
- children<object> - an object with keys that match the expected keys of the value with a valueModel for each key. This allows for deeply nested data structure models.
array
- minLength<number>
- maxLength<number>
- requredKeys<array>
- includes<any> - fail validation if array does not include value
- notIncludes<any> - fail validation if array does include value
- includesAny<any> - fail validation if array does not include any of the values in an array
- notIncludesAny<[any]> - fail validation if array does include any of the values in an array
- allChildren<validationModel> - validates every child against the given validation model
- children<array> - an array of valueModels that corresponds to the expected value in an array. This allows for deeply nested data structure models.
instance
Check to see if a value is an instance of a constructor.
of<Constructor> - the constructor of the instance you are testing. uses instanceof, so values like 'Object' will still pass
strict<bool> - strictly test constructor. Ancestors not included
instance also inherits all of the options of Object, including children!
const Hooligan = class Hooligan {
constructor (name) {
this.name = name
this.stomach = []
this.mood = 'grumpy'
}
eat (food) {
this.stomach.push(food)
}
drink (drank) {
if (drank === 'coffee') this.mood = 'better'
}
}
const Ian = new Hooligan('Ian')
validate(Ian, {
type: 'instance',
of: Hooligan,
strict: true,
children: {
name: String,
eat: Function,
drink: Function
}
})
is
validates that the value is exactly equal to the value specified
validate('true', { type: 'is', exactly: 'true' }) // ==> 'true'
validate('true', { type: 'is', exactly: true }) // throw error
string-int
parseInt then number validation
validator validations
note all of the validations below are powered by the validator package. See the validator docs for more info on the options. Validator options can be provided through the validation object. They should work as expected, but I still need to write tests to verify and need to add better use documentation below. For now, I am just showing an example of the error that will be thrown if validation is not met.
string: ${value} does not match email validation with options: ${options}
postal-code
value: ${value} is not a valid postal code ${state ? ` for state ${state}` : ''}
url
URL ${value} did not match the validation: ${options}
date
value: ${value} is not a valid date
date-obj
value: ${value} is not a valid date object
phone
alpha
string: ${value} is not alpha.
alpha-numeric
value: ${value} is not alpha numeric.
ascii
string: ${value} is not an ascii string.
base64
string: ${value} is not a base64 string.
credit-card
value: ${value} is not a valid credit card number.
currency
value: ${value} is not a valid currency with options: ${options}.
data-uri
value: ${value} is not a valid data URI.
fqdn
value: ${value} is not a valid fully qualified domain name.
float
value: ${value} is not a valid float.
hash
value: ${value} is not a valid ${algorithm} hash.
hex-color
value: ${value} is not a valid hex color.
hex-dec
value: ${value} is not a valid hexadecimal.
ip
value: ${value} is not a valid IP v. ${version}.
isbn
value: ${value} is not a valid ISBN v. ${version}.
issn
value: ${value} is not a valid ISSN with options ${options}.
isin
value: ${value} is not a valid ISIN (stock/security identifier).
iso8601
value: ${value} is not a valid ISO8601.
isrc
value: ${value} is not a valid ISRC.
int
value: ${value} is not a valid int with options: ${options}.
json
value: ${value} is not a valid JSON.
lat-long
value: ${value} is not a valid lat-long.
mac
value: ${value} is not a valid MAC address.
md5
value: ${value} is not a valid MD5 hash.
mime-type
value: ${value} is not a valid mime type.
mongo-id
value: ${value} is not a valid mongo-id.
port
value: ${value} is not a valid port number.
uuid
value: ${value} is not a valid UUID.
or
// pass in an array of validation objects instead of a single object to do an or validation
validate(11 , [String, { Number, max: 22 }]) // ==> 11
validate('towel' , [String, { Number, max: 22 }]) // ==> 'towel'
validate(89 , [String, { Number, max: 22 }]) // throw error
extensions & only
One of the most valuable features of the validationator is the ability to easily add your own validations. Simply define a new type via validate.extensions and throw errors to validate the value.
To add validations define validate.extensions as an object. Every key you add will be a new available type. The value is a method that takes two parameters: the value that will be passed to it in the validation process, and a validation object. This is exactly the same way the validations under the hood are designed. Below are a few examples of existing types to get you started. Remember! You want to throw errors not return true or false. That is handled by the error catching BOOL option area of the validate function.
validate itself throws errors by default, so feel free to validate itself within the validations.
If you create a valuable validation please visit our contribution guidelines page and consider submitting a pull request :)
const validate = require('validationator').validate
validate.extensions = {
'is': (value, { exactly }) => {
if (typeof value !== typeof exactly || JSON.stringify(value) !== JSON.stringify(exactly)) throw new Error(`value: ${value} is not exactly equal to ${exactly}`)
},
'string-int': (value, validation) => {
validate(parseInt(value), Object.assign(
validation,
{ type: 'number', name: 'string-int' }
))
},
'postal-code': (value, { state }) => {
validate(value, ['string', 'number'])
state = state || 'any'
value = `${value}` // convert to string if not number
if (!validator.isPostalCode(value, state)) throw new Error(`value: ${value} is not a valid postal code ${state ? ` for state ${state}` : ''}`)
},
'phone': (value, { mobile }) => {
validate(value, ['string', 'number'])
value = `${value}` // convert to string if not number
validate(value, {
type: 'string',
name: 'phone',
regEx: /^\s*(?:\+?(\d{1,3}))?[-. (]*(\d{3})[-. )]*(\d{3})[-. ]*(\d{4})(?: *x(\d+))?\s*$/
})
if (mobile === true) mobile = 'any'
if (mobile) validate.isMobilePhone(value, mobile)
},
}
// only is like extensions except that it allows you to specifically pick the only validations you want to include in the validate call. You also have access to the default validations object.
// this is here as a performance optimization, but honestly, it is just cutting out a few dozen ops.
import { validate, validations } from 'validationator'
validate.only = {
'number': validations.number,
'postal-code': (value, { state }) => {
validate(value, ['string', 'number'])
state = state || 'any'
value = `${value}` // convert to string if not number
if (!validator.isPostalCode(value, state)) throw new Error(`value: ${value} is not a valid postal code ${state ? ` for state ${state}` : ''}`)
},
}
// number and postal-code are now the only validations available
// you can also use single validations on their own to throw errors
validations.number(12, { max: 10 }) // throws error
// note that bool warn and passthrough options are not available when used this way
validateFunc
using the validate core validateFunc serves as an advanced runtime static type checker and validator. Usage of validateFunc differs slightly depending on the way you structure your parameters. You define the models by adding an inputModel or/and outputModel param to your function. Both inputModel and outputModel are optional, but both cannot be missing or you should not be using the function. You must pass your function then your parameters into validateFunc, or you can build it into your function definitions. validateFunc will return the return value of the product.
validateFunc(func, param(s), options)
options (optional)
The options object can take the following parameters:
- on<boolean> - if true the validate function will ignore the check for a production node environment and run anyway. This is an override option to let the validation run in production.
- off<boolean> - if true turns off all validation. Same as what happens in production environment
- warn<boolean> - if true errors delivered through console.warn rather than throwing an error. This allows for non-blocking validation.
single parameter
const increment = number => (number + 1)
increment.inputModel = Number
increment.outputModel = Number
validateFunc(increment, 1) // ==> 2
validateFunc(increment, '1') // throws error
Or if you were to build it into the function
const increment = number => {
const innerFunc = number => (number + 1)
innerFunc.inputModel = { Number, name: 'increment' }
innerFunc.outputModel = { Number, name: 'increment' }
return validateFunc(innerFunc, number)
}
increment(1) // increment is now a strongly typed function
You could also accomplish the same thing using just the validate core library.
const increment = number => {
validate(number, Number)
const innerFunc = number => (number + 1)
return validate(innerFunc(number), Number)
}
increment(1) // increment is now a strongly typed function
or with the new type syntax
import { type as x } from 'validationator'
const increment = number => {
x(Number, number)
const innerFunc = number => (number + 1)
return x(Number, innerFunc(number))
}
Note that if your function takes in a single array as its sole argument you will need to double wrap the array so that validateFunc does not think it is for multiple parameters.
const ct = arr => arr.length
ct.inputModel = { Array }
validateFunc(ct, [[ 1, 2, 3 ]])
multiple parameters
const multiply = (a, b) => a * b
multiply.inputModel = [ Number, Number ]
assert(validateFunc(multiply, [2, 3]) === 6)
parameter destructuring
const multiply = ({ a, b }) => a * b
multiply.inputModel = {
a: Number,
b: Number,
}
validateFunc(multiply, { a: 1, b: 8 })
it just works
// do below the function like react proptypes
const testFunc = (num, char, bool) => {
return {
see: 'it works',
coolness: 1000000,
itSucks: false,
}
}
testFunc.inputModel = [
{ Number, min: 40 },
{ String, maxLength: 1 },
{ Boolean, notRequired: true },
]
testFunc.outputModel = {
Object,
children: {
see: String
coolness: { Number, min: 10000 },
itSucks: { Boolean, acceptedNulls: [ false ] },
},
}
validateFunc(testFunc, [ 50, 'a', false ])
// or contained within the function itself
const testFunc2 = (num, char, bool) => {
const execution = (num, char, bool) => ({
see: 'it works',
coolness: 1000000,
itSucks: false,
})
execution.inputModel = [
{ Number, min: 40 },
{ String, maxLength: 1 },
{ Boolean, notRequired: true },
]
execution.outputModel = {
Object,
children: {
see: String,
coolness: { Number, min: 10000 },
itSucks: { Boolean, acceptedNulls: [ false ] },
},
}
return validateFunc(execution, [num, char, bool])
}
testFunc2(50, 'a', false) // now a strongly typed function