antonioru / deep-waters

πŸ”₯Deep Waters is an easy-to-compose functional validation system for javascript developers πŸ”₯
https://antonioru.gitbook.io/deep-waters/
MIT License
199 stars 9 forks source link

POC - Objectizing and enriching validation responses #3

Open sodiray opened 4 years ago

sodiray commented 4 years ago

@antonioru I know your planning to tackle this this weekend. Couldn't get it off my mind and had some free time so heres a proof of concept script.

There's a lot of room for improvement and maybe some optimizations. Hope its helpful. Let me know what you think.

/**
* All around functional helpers
*/
const compose = (...funcs) => (...args) => funcs.slice(1).reduce((acc, fn) => fn(acc), funcs[0](...args))
const partial = (fn, ...args) => (...rest) => fn(...args, ...rest)

/**
* Function for converting validator function
* boolean results to an object
*/
const objectize = (doc, fn, value) => ({
  ...doc,
  valid: fn(value),
  value
})

/**
* When composing many validator function this will
* aggregate into passed and failed results
*/
const aggregateResults = (results) => ({
  passed: results.filter(r => r.valid), 
  failed: results.filter(r => !r.valid)
})

/**
* Some more helpers for composing and/or validations
*/
const applyDoc = (doc) => (result) => ({ ...result, ...doc })
const applyValue = (value) => (result) => ({ ...result, value })
const runValidators = (validators) => (value) => validators.map(validator => validator(value))

/**
* Determines if a result of composed functions
* is valid or not
*/
const orValid = (result) => ({ ...result, valid: result.passed.length > 0 })
const andValid = (result) => ({ ...result, valid: result.failed.length === 0 })

/**
* HOF used to take simple bool validation functions and
* convert to full object style validators
*/
const validator = (fn) => (doc) => partial(objectize, doc, fn)

// const orValidator = (...validators) => (doc) => value => orValid(applyDoc(doc)(aggregateResults(runValidators(validators)(value))))
const orValidator = (...validators) => (doc) => (value) => compose(
  runValidators(validators),
  aggregateResults,
  applyDoc(doc),
  applyValue(value),
  orValid)(value)

const andValidator = (...validators) => (doc) => (value) => compose(
  runValidators(validators), 
  aggregateResults,
  applyDoc(doc),
  applyValue(value),
  andValid)(value)

/**
* Same as before - bool validator functions. Some of these
* abbreviated for POC sake. Validators that were previously
* composed at this level would now be composed further
* below as validators
*/
const ofClass = (name, value) => Object.prototype.toString.call(value) === `[object ${name}]`
const isFalse = value => value === false
const isZero = value => value === 0
const isNumber = partial(ofClass, 'Number')
const biggerThan = number => value => value > number
const smallerThan = number => value => value < number

/**
* Object style validators composed with
* the core bool validators and descripting
* objects
*/
const isZeroValidator = validator(isZero)({
  name: 'isZero'
})

const isFalseValidator = validator(isFalse)({
  name: 'isFalse'
})

const isNumberValidator = validator(isNumber)({
  name: 'isNumber'
})

const biggerThanValidator = (min) => validator(biggerThan(min))({
  name: 'biggerThan',
  expected: `number bigger than ${min}`
})

const smallerThanValidator = (max) => validator(smallerThan(max))({
  name: 'smallerThan',
  expected: `number smaller than ${max}`
})

const isFalsyValidator = orValidator(isZeroValidator, isFalseValidator)({
  name: 'isFalsy'
})

const betweenValidator = (min) => (max) => andValidator(biggerThanValidator(min), smallerThanValidator(max))({
  name: 'betweenValidator'
})

/**
*
* Some POC Tests :smile:
*
*/

const assert = (condition) => {
  if (!condition) throw 'Assertion failed'
}

const isZeroResult = isZeroValidator(0)
console.log('isZeroResult ===>', isZeroResult)
assert(isZeroResult.name === 'isZero')
assert(isZeroResult.valid === true)
assert(isZeroResult.value === 0)

const isFalseResult = isFalseValidator(true)
console.log('isFalseResult ===>', isFalseResult)
assert(isFalseResult.name === 'isFalse')
assert(isFalseResult.valid === false)
assert(isFalseResult.value === true)

const biggerThanResult = biggerThanValidator(10)(50)
console.log('biggerThanResult ===>', biggerThanResult)
assert(biggerThanResult.name === 'biggerThan')
assert(biggerThanResult.valid === true)
assert(biggerThanResult.value === 50)
assert(biggerThanResult.expected === 'number bigger than 10')

const isFalsyResult = isFalsyValidator(0)
console.log('isFalsyResult ===>', isFalsyResult)
assert(isFalsyResult.name === 'isFalsy')
assert(isFalsyResult.valid === true)
assert(isFalsyResult.value === 0)
assert(isFalsyResult.passed[0].name === 'isZero')

const betweenResult = betweenValidator(10)(20)(9)
console.log('betweenResult ===>', betweenResult)
assert(betweenResult.name === 'betweenValidator')
assert(betweenResult.valid === false)
assert(betweenResult.value === 9)
assert(betweenResult.passed[0].name === 'smallerThan')

Also, wasn't sure how to share this with you... hope issue is ok πŸ‘β“

antonioru commented 4 years ago

Hi @rayepps

first let me tell you, your code is absolutely brilliant, you must be a very talented developer! I've developed something similar by using a monad to compose the responses, you can find my solution in the branch feature/monadic-response (of course it's way worse than this)

If you don't mind, I'll respond you properly in the #1 issue, so we'll keep the discussion there πŸ˜‰


PS: in case you need to share anything with me in future, and I would be glad of it, my email address is:

my own name (antonio) AT my own company name (beautifulinteractions) .com ))