Skip to content

Commit

Permalink
Refactor everything - include typeError messaging as a validation err…
Browse files Browse the repository at this point in the history
…or, include transforms in ast, include funky validators (negate, serial, oneOfType) in ast
  • Loading branch information
akmjenkins committed Mar 15, 2020
1 parent db03336 commit 875191c
Show file tree
Hide file tree
Showing 38 changed files with 466 additions and 115 deletions.
9 changes: 8 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ module.exports = {
node: {
paths: ['./src'],
},
alias: {
map: [
['@zuze/schema','./src']
]
}
}
},
extends: [
Expand All @@ -38,7 +43,9 @@ module.exports = {
'import/namespace': 0,
'import/no-self-import': 2,
'import/first': 2,
'import/order': 2,
'import/order': ['error',{
'newlines-between':'never'
}],
'import/no-named-as-default': 0
},
overrides: [
Expand Down
3 changes: 3 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
module.exports = {
moduleNameMapper: {
'@zuze/schema': '<rootDir>/src',
},
coverageThreshold: {
global: {
branches: 90,
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "Composable validation schema, inspired by yup/joi",
"main": "build/cjs/index.js",
"module": "build/esm/index.js",
"sideEffects": false,
"files": [
"build"
],
Expand Down Expand Up @@ -42,6 +43,7 @@
"coveralls": "^3.0.9",
"eslint": "^6.4.0",
"eslint-config-prettier": "^6.3.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-jest": "^22.17.0",
"eslint-plugin-prettier": "^3.1.0",
Expand Down
15 changes: 15 additions & 0 deletions src/ast/astToFn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const processor = (fn, ...args) => fn(...args);

export default (property, source, err, process = processor) => (
ast,
options
) => {
const [name, ...args] = Array.isArray(ast) ? ast : [ast];
const userDefined = options[property][name];
const fn = userDefined || source[name];
if (!fn) throw new Error(err(name));

// userDefined transforms/validators must always
// be a function that accepts options and returns a transform/validator factory
return process(userDefined ? fn(options) : fn, ...args);
};
42 changes: 37 additions & 5 deletions src/ast/createSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,25 @@ import array from '../array';
import string from '../string';
import mixed from '../mixed';
import { condition } from '../conditions';
import { createValidators } from './createValidator';
import { createValidators } from './createValidators';
import { createTransforms } from './createTransforms';
import * as astTransforms from './transforms';
import * as astValidators from './validators';

const schemas = { object, number, boolean, date, array, string, mixed };

const defaults = (options = {}) => ({
...options,
transforms: {
...astTransforms,
...(options.transforms || {}),
},
validators: {
...astValidators,
...(options.validators || {}),
},
});

const make = (type, def, options = {}) => {
const use = type || 'mixed';
if (!schemas[use]) throw new Error(`Can't create schema ${type}`);
Expand All @@ -24,14 +39,29 @@ export const createSchemas = (defs, options) =>
(Array.isArray(defs) ? defs : [defs]).map(d => createSchema(d, options));

export const createSchema = (
{ schema, tests = [], default: def, meta, label, shape, of, conditions },
{
schema,
tests = [],
transforms = [],
default: def,
meta,
typeError,
label,
shape,
of,
nullable,
conditions,
},
options
) =>
make(
) => {
options = defaults(options);
return make(
schema,
{
default: () => def,
label,
label: () => label,
nullable,
typeError,
condition: conditions ? conditions.map(condition) : undefined,
shape: shape
? Object.entries(shape).reduce(
Expand All @@ -45,6 +75,8 @@ export const createSchema = (
of: of ? createSchema(of, options) : undefined,
meta,
test: createValidators(tests, options),
transform: createTransforms(transforms, options),
},
options
);
};
11 changes: 11 additions & 0 deletions src/ast/createTransforms.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as transforms from '../transforms';
import astToFn from './astToFn';

const processor = astToFn(
'transforms',
transforms,
name => `No transform found for ${name}`
);

export const createTransforms = (ast, options) =>
ast.map(v => processor(v, options));
13 changes: 0 additions & 13 deletions src/ast/createValidator.js

This file was deleted.

15 changes: 15 additions & 0 deletions src/ast/createValidators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as validators from '../validators';
import { recursivelyConvertRefs } from './utils';
import astToFn from './astToFn';

const processor = astToFn(
'validators',
validators,
name => `No validator found for ${name}`,
(fn, ...args) => fn(...recursivelyConvertRefs(...args))
);

export const createValidators = (ast, options) =>
ast.map(v => createValidator(v, options));

export const createValidator = (ast, options) => processor(ast, options);
1 change: 0 additions & 1 deletion src/ast/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export { default as matches } from './matches';
export { createSchemas, createSchema } from './createSchema';
export { createValidators, createValidator } from './createValidator';
3 changes: 3 additions & 0 deletions src/ast/transforms/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// these transforms require special handling in ast-land
export { default as reject } from './reject';
export { default as unique } from './unique';
4 changes: 4 additions & 0 deletions src/ast/transforms/reject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import matches from '../matches';

export default options => where => value =>
Array.isArray(value) ? value.filter(v => !matches(where, v, options)) : value;
11 changes: 11 additions & 0 deletions src/ast/transforms/unique.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { getter } from 'property-expr';
const def = (a, b) => b === a;

const createBy = using =>
using ? (a, b) => getter(using, true)(a) === getter(using, true)(b) : def;

export default () => by => value => {
if (!Array.isArray(value)) return value;
const fn = createBy(by);
return value.filter((a, idx, arr) => arr.findIndex(b => fn(a, b)) === idx);
};
4 changes: 4 additions & 0 deletions src/ast/validators/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// these validators require special handling in ast-land
export { default as negate } from './negate';
export { default as oneOfType } from './oneOfType';
export { default as serial } from './serial';
4 changes: 4 additions & 0 deletions src/ast/validators/negate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { negate } from '../../validators';
import { createValidator } from '../createValidators';

export default options => def => negate(createValidator(def, options));
5 changes: 5 additions & 0 deletions src/ast/validators/oneOfType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { oneOfType } from '../../validators';
import { createSchema } from '../createSchema';

export default options => types =>
oneOfType(types.map(t => createSchema(t, options)));
5 changes: 5 additions & 0 deletions src/ast/validators/serial.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { serial } from '../../validators';
import { createValidator } from '../createValidators';

export default options => defs =>
serial(...defs.map(d => createValidator(d, options)));
58 changes: 33 additions & 25 deletions src/cast.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,56 @@ import { defaults } from './utils';
import resolve from './resolve';
import castInner from './cast.inner';

const check = (value, { typeCheck, type }, { assert }) => {
if (value !== undefined && assert && typeCheck && !typeCheck(value))
const check = (
value,
{ typeCheck, type, nullable, label },
{ assert, path }
) => {
if (!assert) return value;

if (value === null) {
if (nullable) return value;
throw new TypeError(
`null given for non-nullable schema ${label || type} ${
path ? `at ${path}` : ``
}`
);
}

if (value !== undefined && typeCheck && !typeCheck(value))
throw new TypeError(
`cast value of ${value} is not a valid type for schema ${type}`
);

return value;
};

const cast = (schema, value, options) => {
options = defaults(options);
const fork = resolve(schema, { ...options, value });
if (fork !== schema) return cast(fork, value, options);

const { strict, assert, path } = options;
const { transform, inner, default: def, nullable, label, type } = schema;

// nullable check
if (value === null) {
if (nullable) return value;
if (assert)
throw new Error(
`null given for non-nullable schema ${label || type} ${
path ? `at ${path}` : ``
}`
);
}

const innerSchema = inner && inner(schema);
value = innerSchema ? castInner(innerSchema, schema, value, options) : value;
const { strict } = options;
const { transform, inner, default: def } = schema;

const final =
value = check(
// transforms are ignored when value is undefined or in strict mode
value === undefined || strict
? value
: transform.reduce(
(acc, fn) => fn(acc, value, { schema, options }),
value
);

// check one more time
return (
check(final, schema, options),
final !== undefined ? final : typeof def === 'function' ? def() : def
),
schema,
options
);

const innerSchema = inner && inner(schema);
const final = innerSchema
? castInner(innerSchema, schema, value, options)
: value;

return final !== undefined ? final : typeof def === 'function' ? def() : def;
};

export default cast;
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export { default as lazy } from './lazy';
export { default as ref, isRef } from './ref';
export { default as conditional } from './conditional';
export * from './validators';
export * from './conditions';
export { when, conditions, condition } from './conditions';
export * from './without';
export {
warnings,
Expand All @@ -38,4 +38,5 @@ export {
nullable,
meta,
errors,
typeError,
} from './utils';
4 changes: 2 additions & 2 deletions src/transforms/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { default as string } from './string';
export { default as string, trim, uppercase, lowercase } from './string';
export { default as boolean } from './boolean';
export { default as date } from './date';
export { default as number } from './number';
Expand All @@ -11,5 +11,5 @@ export {
stripKeys,
stripUnknown,
} from './object';
export { default as array, compact, unique } from './array';
export { default as array, compact, unique, exclude, only } from './array';
export * from './utils';
5 changes: 5 additions & 0 deletions src/transforms/string.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
// nullables and undefined's will never get pased to this function
export default v => v.toString();

export const trim = () => v => v && v.trim();
export const strip = () => v => v && v.replace(/\s/g);
export const uppercase = () => v => v && v.toUpperCase();
export const lowercase = () => v => v && v.toLowerCase();
3 changes: 3 additions & 0 deletions src/utils/definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const isSchema = ({ __isSchema } = {}) => !!__isSchema;

export default ({
type,
typeError,
typeCheck,
test = [],
transform = [],
Expand All @@ -21,6 +22,7 @@ export default ({
typeCheck: typeCheck ? typeCheck : () => true,
type: type || MIXED,
test,
typeError,
transform,
condition,
meta,
Expand Down Expand Up @@ -85,6 +87,7 @@ export const merge = (def, ...defs) =>
(acc, def) => ({
__isSchema: true,
...mergeTypes(def, acc),
typeError: acc.typeError || def.typeError,
test: filterValidators(filterSame(acc.test, def.test)),
transform: filterSame(acc.transform, def.transform),
condition: filterSame(acc.condition, def.condition),
Expand Down
27 changes: 18 additions & 9 deletions src/utils/errors.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
export default (validationError, multiple = false) =>
validationError
? validationError.inner.reduce(
(acc, { path, message }) => ({
...acc,
[path]: multiple
? [...(acc[path] || []), message]
: acc.path
? acc.path
: message,
}),
? Object.entries(
validationError.inner.reduce(
(acc, { path, message }) => ({
...acc,
[path]: multiple
? [...(acc[path] || []), message]
: acc.path
? acc.path
: message,
}),
{}
)
).reduce(
// if we are getting errors from something with a non-inner
// schema then just return the errors, otherwise return a map
// of the errors by path
(acc, [path, errs]) =>
path === 'undefined' ? errs : { ...acc, [path]: errs },
{}
)
: {};

0 comments on commit 875191c

Please sign in to comment.