Skip to content

Instantly share code, notes, and snippets.

@rhythnic
Last active October 18, 2019 05:37
Show Gist options
  • Save rhythnic/945299d10008cc571d649926f3a37961 to your computer and use it in GitHub Desktop.
Save rhythnic/945299d10008cc571d649926f3a37961 to your computer and use it in GitHub Desktop.
// ***********************************************************************
// Validator Lib
// ***********************************************************************
const { Success, Failure } = require('folktale/validation')
const fieldPath = (...args) => args.join('.')
const optional = args => ([
(path, prop) => {
let assertions = args.map(x => x(path, prop))
return data => data[prop] == null
? Success(data)
: assertions.reduce((acc, x) => acc.concat(x(data)), Success(data))
}
])
const rootAssertions = (model, path) => Object.keys(model).reduce(
(acc, prop) => typeof model[prop] === 'function'
? acc
: [ ...acc, ...model[prop].map(x => x(path, prop))]
,
[]
)
const struct = (model, customValidation) => path => {
const assertions = rootAssertions(model, path)
const nested = Object.keys(model)
.filter(prop => typeof model[prop] === 'function')
.map(prop => ({ prop, assertion: model[prop](fieldPath(path, prop)) }))
if (!customValidation) customValidation = data => Success(data)
return data => assertions
.reduce(
(acc, assertion) => acc.concat(assertion(data)),
nested.reduce((acc, x) => acc.concat(x.assertion(data[x.prop])), Success())
)
.concat(customValidation(data))
.map(_ => data)
}
// ***********************************************************************
// Validators
// ***********************************************************************
const isType = type => (path, prop) => data =>
typeof data[prop] === type
? Success(data)
: Failure([`${fieldPath(path, prop)} must have type ${type}`])
const minLength = min => (path, prop) => data =>
data[prop].length >= min
? Success(data)
: Failure([`${fieldPath(path, prop)} must have a minimum length of ${min}`])
const hasLength = len => (path, prop) => data =>
data[prop].length === len
? Success(data)
: Failure([`${fieldPath(path, prop)} must have length ${len}`])
const equals = val => (path, prop) => data =>
data[prop] === val
? Success(data)
: Failure([`${fieldPath(path, prop)} must equal ${val}`])
const isArray = (path, prop) => data =>
Array.isArray(data[prop])
? Success(data)
: Failure(`${fieldPath(path, prop)} must be an array`)
const items = modelStruct => (path, prop) => data =>
data[prop].reduce(
(acc, item, i) => acc.concat(modelStruct(fieldPath(path, prop, i))(item)),
Success(data)
)
// ***********************************************************************
// Curried Validators
// ***********************************************************************
const isString = isType('string')
// ***********************************************************************
// API
// ***********************************************************************
module.exports = {
struct,
optional,
items,
isArray,
isType,
isString,
minLength,
hasLength,
equals
}
// ***********************************************************************
// Example Usage
// ***********************************************************************
const idAssertions = [isString, minLength(2)]
const userSchema = {
username: optional(isString, minLength(2)),
pwdHash: [isString, hasLength(16)],
email: optional(isString, minLength(6)),
company: struct({
id: idAssertions,
type: equals('Company')
}),
friends: [
isArray,
items(struct({
id: idAssertions,
type: equals('User')
}))
]
}
const User = struct(userSchema, data => {
// any custom validation here
if (!data.email && !data.username) {
return Failure(['username required if email not present'])
}
return Success(data)
})
const validateUser = User()
// validateUser(data)
// Success(data) or
// Failure(['username must be a string', 'friends.0.type must equal User'])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment