|
/** |
|
* Internal helpers |
|
*/ |
|
|
|
// Predicates can return true/false or they can return an error message. |
|
// true, null, undefined and empty string are considered a valid result |
|
function isValid (result) { |
|
return result === true || result === null || result === void 0 || result === ''; |
|
} |
|
|
|
function validate (name, predicate, x) { |
|
const result = predicate(x); |
|
|
|
if (result === false) { |
|
return `Expected "${name}", but received "${JSON.stringify(x)}"`; |
|
} |
|
|
|
if (isValid(result)) return void 0; |
|
|
|
if (typeof result === 'string') { |
|
return `[${name}]: ${result}`; |
|
} |
|
|
|
if (typeof result === 'object' && result.name && result.message) { |
|
return `[${name}/${result.name}]: ${result.message}`; |
|
} |
|
|
|
return void 0; |
|
} |
|
|
|
// Helper for joining multiple predicates provided as an array |
|
function composer (type, fns) { |
|
return function predicate (x) { |
|
let result; |
|
for (let i = 0, len = fns.length; i < len; i++) { |
|
result = fns[i](x); |
|
const valid = isValid(result); |
|
if (valid && type === 'some') return result; |
|
if (!valid && type === 'every') return result; |
|
} |
|
|
|
// For `some`, nothing was valid up to here; so invalid |
|
if (type === 'some') return result || false; |
|
|
|
// For `every` everything was valid; so valid |
|
return void 0; |
|
}; |
|
} |
|
|
|
export const every = composer.bind(null, 'every'); |
|
export const some = composer.bind(null, 'some'); |
|
|
|
/** |
|
* Public api |
|
*/ |
|
|
|
const Methods = { |
|
is (x) { |
|
return !validate(this.meta.name, this.meta.predicate, x); |
|
}, |
|
|
|
expand (name, arg) { |
|
// Allow multiple expansions under one name |
|
const predicate = Array.isArray(arg) ? some(arg) : arg; |
|
|
|
// eslint-disable-next-line no-use-before-define |
|
return def(name, some([ |
|
validate.bind(null, name, predicate), |
|
validate.bind(null, this.meta.name, this.meta.predicate), |
|
])); |
|
}, |
|
|
|
refine (name, arg) { |
|
// Allow multiple refinements under one name |
|
const predicate = Array.isArray(arg) ? every(arg) : arg; |
|
|
|
// eslint-disable-next-line no-use-before-define |
|
return def(name, every([ |
|
validate.bind(null, name, predicate), |
|
validate.bind(null, this.meta.name, this.meta.predicate) |
|
])); |
|
} |
|
}; |
|
|
|
/** |
|
* Constructor |
|
*/ |
|
|
|
export default function def (name, arg) { |
|
const predicate = Array.isArray(arg) ? every(arg) : arg; |
|
|
|
const T = function Type (x) { |
|
const err = validate(Type.meta.name, Type.meta.predicate, x); |
|
if (err && process.env.NODE_ENV !== 'production') throw new TypeError(err); |
|
return x; |
|
}; |
|
|
|
Object.defineProperties(T, { |
|
is: { value: Methods.is.bind(T) }, |
|
expand: { value: Methods.expand.bind(T) }, |
|
refine: { value: Methods.refine.bind(T) }, |
|
name: { value: name }, |
|
meta: { value: { name, predicate } }, |
|
}); |
|
|
|
return T; |
|
} |
|
|
|
/** |
|
* Some pre-defined types |
|
*/ |
|
|
|
export const Nil = def('Nil', x => x === null || x === void 0); |
|
export const Arr = def('Array', x => Array.isArray(x)); |
|
export const Obj = def('Object', x => !Nil.is(x) && !Array.isArray(x) && typeof x === 'object'); |
|
export const Str = def('String', x => typeof x === 'string'); |
|
export const Num = def('Number', x => Number.isFinite(x)); |
|
export const Int = Num.refine('Integer', x => Number.isInteger(x)); |
|
|
|
/** |
|
* Convenience |
|
*/ |
|
|
|
export function optional (Type) { |
|
return Type.expand(`${Type.meta.name}?`, Nil.is); |
|
} |
|
|
|
export function properties (required) { |
|
const keys = Object.keys(required); |
|
return function keysPredicate (x) { |
|
if (!Obj.is(x)) { |
|
return `Expected an object, but received "${typeof x}"`; |
|
} |
|
|
|
for (let i = 0, len = keys.length; i < len; i++) { |
|
const k = keys[i]; |
|
const Type = required[k]; |
|
const err = validate(Type.meta.name, Type.meta.predicate, x[k]); |
|
if (err) return { name: k, message: err }; |
|
} |
|
|
|
return void 0; |
|
}; |
|
} |
|
|
|
properties.oneOf = function oneOf (args) { |
|
return function hasAtLeastOne (x) { |
|
for (let i = 0, len = args.length; i < len; i++) { |
|
if (x.hasOwnProperty(args[i])) return true; |
|
} |
|
return `one of ${args.join('/')} is required`; |
|
}; |
|
}; |