/** @module ego */
/** @namespace module:ego~spec */
import fp from './like.lodash'
import { Context } from './context'
const SPEC_FN_SYMBOL = Symbol('spec-fn-symbol')
const LazySpec = class {
constructor(lazy) {
this.lazy = lazy
}
static isLazy(value) {
return value instanceof LazySpec
}
}
const nullOrCollection = fp.cond([
[fp.isEmpty, () => null],
[fp.stubTrue, fp.identity],
])
const isSpec = spec => Boolean(spec[SPEC_FN_SYMBOL])
const asSpec = (specFn) => {
specFn[SPEC_FN_SYMBOL] = true
return Object.freeze(specFn)
}
export default {
/**
* Usage of function makes sense only if u have spec flow outside of object context
* or collection context and want to rename output label for validation message
* @function designate
* @memberof module:ego~spec
*
* @arg {string} key - key to use instead of 'value' for label to output validation message
* @arg {function} spec - Accpets spec function
*
* @returns {function} - Spec function which could be used as argument for lib functions
*
* @example
*
* import { spec } from 'egoist-js'
*
* const asUserSpec = spec.designate('user')
*
* const userModelSpec = spec.compose(
* asUserSpec(spec.flow(required)),
* spec.of({
* username: spec.flow(isNotEmpty, required),
* friends: spec.of([spec.lazy(() => userModelSpec)]),
* })
* )
*
* const result = validate(null, { untilFail: true })
* // [{ message: 'user is required', value: null, args: undefined, path: [] }]
*
*/
designate: fp.curry((alias, spec) => asSpec((value, options = {}) => {
const refinedOpts = {
...options,
context: Context.from({
...options.context,
alias,
}),
}
return spec(value, refinedOpts)
})),
/**
* Function is used to create a lazy evaluate specification item
* @function lazy
* @memberof module:ego~spec
*
* @arg {function} getSpecLazy - Expected spec function to be passed
*
* @returns {LazySpec} - Object which could be used as argument for #compose, #of functions
*
* @example
*
* import { spec } from 'egoist-js'
*
* const userModelSpec = spec.of({
* username: spec.flow(string.isNotEmpty, any.required),
* bestFriend: spec.lazy(() => userModelSpec), // returns lazy evaluated spec function
* })
*
*/
lazy: (getSpecLazy) => {
if (!fp.isFunction(getSpecLazy)) {
throw new Error('lazy expects function as an argument')
}
return new LazySpec(getSpecLazy)
},
/**
* Function is used to create validation flow
* @function flow
* @memberof module:ego~spec
*
* @arg {function[]} ...validators - Accepts validation functions as list of arguments
*
* @returns {function} - Spec function which could be used as argument
*
* @example
*
* import { spec } from 'egoist-js'
*
* const usernameSpec = spec.flow(
* string.isString,
* string.match(/^[A-Z][a-z]+\s[A-Z][a-z]+$/)
* string.isNotMatch,
* any.required
* )
*
*/
flow: (...validators) => asSpec((value, options) => {
const results = []
const { context, untilFail } = fp.assign({
context: null,
untilFail: false,
}, options)
for (let i = 0, l = validators.length; i < l; i += 1) {
const validator = validators[i]
const result = validator(value, { context })
if (fp.isArray(result)) {
results.push({
result,
value,
context,
})
}
if (untilFail && results.length > 0) {
return results
}
}
return nullOrCollection(results)
}),
/**
* Function is used to create a composition between specification functions
* @function compose
* @memberof module:ego~spec
*
* @arg {function[]} ...specs - Accpets spec functions as list of arguments
*
* @returns {function} - Spec function which could be used as argument
*
* @example
*
* import { spec } from 'egoist-js'
*
* const userModelBodySpec = spec.of({
* username: spec.flow(string.isNotEmpty, any.required),
* bestFriend: spec.lazy(() => userModelBodySpec),
* })
*
* const userModelRequiredSpec = spec.flow(any.requied)
*
* const fullUserModelSpec = spec.compose(
* userModelRequiredSpec,
* spec.flow(shape.expectKeys(['username', 'bestFriend'])),
* userModelBodySpec,
* )
*
*/
compose: (...specs) => {
if (!specs.every(isSpec)) {
throw new Error('compose args should be the specs')
}
return asSpec((value, options) => {
const results = []
const extOptions = fp.assign({
context: null,
untilFail: false,
}, options)
for (let i = 0, l = specs.length; i < l; i += 1) {
const spec = specs[i]
const result = spec(value, extOptions)
if (fp.isArray(result)) {
results.push(...result)
}
if (extOptions.untilFail && results.length > 0) {
return results
}
}
return nullOrCollection(results)
})
},
// TODO: mb use template method to support diff forms of spec generators
/**
* Function helps to create spec fof complex object such as arrays or shapes (aka objects)
* @function of
* @memberof module:ego~spec
*
* @arg {Object|Array} specDescriptor - Accepts object where properties' values must be spec functions
* or array with only one item that is a spec function
*
* @returns {function} - Spec function which could be used as argument
*
* @example
*
* import { spec } from 'egoist-js'
*
* const simpleHeroSpec = spec.of({
* name: spec.flow(string.isString, string.isNotEmpty, any.required),
* abilities: spec.of([spec.flow(any.required)])
* address: spec.of({
* city: spec.flow(any.required),
* // pls note - spec can be complex
* streets: spec.of([spec.of({ zip: spec.flow(any.required) })])
* })
* })
*
*/
of: specDescriptor => {
if (fp.isArray(specDescriptor)) {
// spec generator for arrays aka collections
return asSpec((collection, options) => {
const results = []
const { context, untilFail } = fp.assign({
context: null,
untilFail: false,
}, options)
const [spec] = specDescriptor
const specFn = LazySpec.isLazy(spec) ? spec.lazy() : spec
if (!fp.isArray(collection)) {
return null
}
for (let i = 0, l = collection.length; i < l; i += 1) {
const value = collection[i]
const result = specFn(value, {
context: Context.create(collection, context, i),
untilFail,
})
if (fp.isArray(result)) {
results.push(...result)
}
if (untilFail && results.length > 0) {
return results
}
}
return nullOrCollection(results)
})
}
// spec generator for objects aka shapes
return asSpec((value, options) => {
const results = []
const { context, untilFail } = fp.assign({
context: null,
untilFail: false,
}, options)
const kvPairs = fp.entries(specDescriptor)
for (let i = 0, l = kvPairs.length; i < l; i += 1) {
const [key, spec] = kvPairs[i]
const specFn = spec.lazy ? spec.lazy() : spec
const result = specFn(fp.get(key, value), {
context: Context.create(value, context, key),
untilFail,
})
if (fp.isArray(result)) {
results.push(...result)
}
if (untilFail && results.length > 0) {
return results
}
}
return nullOrCollection(results)
})
},
}