Skip to content

Commit

Permalink
feat: finalize resolve() (#447)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: reach() no longer resolves the returned schema meaning it's conditions have not been processed yet; prefer validateAt/castAt where it makes sense
  • Loading branch information
vonagam authored and jquense committed Mar 14, 2019
1 parent 5b01f18 commit afc5119
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 67 deletions.
75 changes: 42 additions & 33 deletions src/Condition.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,64 @@
import has from 'lodash/has';
import isSchema from './util/isSchema';

function callOrConcat(schema) {
if (typeof schema === 'function') return schema;

return base => base.concat(schema);
function wrapCusomFn(fn) {
return function(...args) {
args.pop();
return fn.apply(this, args);
};
}

class Conditional {
constructor(refs, options) {
let { is, then, otherwise } = options;
function makeFn(options) {
if (typeof options === 'function') return wrapCusomFn(options);

this.refs = [].concat(refs);
if (!has(options, 'is'))
throw new TypeError('`is:` is required for `when()` conditions');

then = callOrConcat(then);
otherwise = callOrConcat(otherwise);
if (!options.then && !options.otherwise)
throw new TypeError(
'either `then:` or `otherwise:` is required for `when()` conditions',
);

if (typeof options === 'function') this.fn = options;
else {
if (!has(options, 'is'))
throw new TypeError('`is:` is required for `when()` conditions');
let { is, then, otherwise } = options;

if (!options.then && !options.otherwise)
throw new TypeError(
'either `then:` or `otherwise:` is required for `when()` conditions',
);
let check;
if (typeof is === 'function') {
check = is;
} else {
check = (...values) => values.every(value => value === is);
}

let isFn =
typeof is === 'function'
? is
: (...values) => values.every(value => value === is);
let fn = function(...args) {
let options = args.pop();
let schema = args.pop();
let branch = check(...args) ? then : otherwise;

this.fn = function(...values) {
let currentSchema = values.pop();
let option = isFn(...values) ? then : otherwise;
if (!branch) return undefined;
if (typeof branch === 'function') return branch(schema);
return schema.concat(branch.resolve(options));
};

return option(currentSchema);
};
}
return fn;
}

class Condition {
constructor(refs, options) {
this.refs = refs;
this.fn = makeFn(options);
}

resolve(ctx, options) {
resolve(base, options) {
let values = this.refs.map(ref => ref.getValue(options));

let schema = this.fn.apply(ctx, values.concat(ctx));
let schema = this.fn.apply(base, values.concat(base, options));

if (schema === undefined || schema === base) return base;

if (schema !== undefined && !isSchema(schema))
if (!isSchema(schema))
throw new TypeError('conditions must return a schema object');

return schema || ctx;
return schema.resolve(options);
}
}

export default Conditional;
export default Condition;
11 changes: 6 additions & 5 deletions src/Lazy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ import isSchema from './util/isSchema';

class Lazy {
constructor(mapFn) {
this._resolve = (...args) => {
let schema = mapFn(...args);
this._resolve = (value, options) => {
let schema = mapFn(value, options);

if (!isSchema(schema))
throw new TypeError('lazy() functions must return a valid schema');

return schema;
return schema.resolve(options);
};
}
resolve({ value, ...rest }) {
return this._resolve(value, rest);
resolve(options) {
return this._resolve(options.value, options);
}
cast(value, options) {
return this._resolve(value, options).cast(value, options);
Expand Down
16 changes: 12 additions & 4 deletions src/mixed.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,22 @@ const proto = (SchemaType.prototype = {
},

resolve(options) {
if (this._conditions.length) {
return this._conditions.reduce(
let schema = this;

if (schema._conditions.length) {
let conditions = schema._conditions;

schema = schema.clone();
schema._conditions = [];
schema = conditions.reduce(
(schema, condition) => condition.resolve(schema, options),
this,
schema,
);

schema = schema.resolve(options);
}

return this;
return schema;
},

cast(value, options = {}) {
Expand Down
6 changes: 1 addition & 5 deletions src/util/reach.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function getIn(schema, path, value, context) {
return {
parent,
parentPath: path,
schema: schema.resolve({ context, parent, value }),
schema,
};

forEach(path, (_part, isBracket, isArray) => {
Expand Down Expand Up @@ -55,10 +55,6 @@ export function getIn(schema, path, value, context) {
}
});

if (schema) {
schema = schema.resolve({ context, parent, value });
}

return { schema, parent, parentPath: lastPart };
}

Expand Down
23 changes: 23 additions & 0 deletions test/mixed.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ref,
reach,
bool,
lazy,
ValidationError,
} from '../src';

Expand Down Expand Up @@ -799,6 +800,28 @@ describe('Mixed Types ', () => {
await inst.validate(-1).should.be.fulfilled();
});

it('should allow nested conditions and lazies', async function() {
let inst = string().when('$check', {
is: value => typeof value === 'string',
then: string().when('$check', {
is: value => /hello/.test(value),
then: lazy(() => string().min(6)),
}),
});

await inst
.validate('pass', { context: { check: false } })
.should.be.fulfilled();

await inst
.validate('pass', { context: { check: 'hello' } })
.should.be.rejectedWith(ValidationError, /must be at least/);

await inst
.validate('passes', { context: { check: 'hello' } })
.should.be.fulfilled();
});

it('should use label in error message', async function() {
let label = 'Label';
let inst = object({
Expand Down
8 changes: 6 additions & 2 deletions test/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,8 +448,12 @@ describe('Object types', () => {
}),
});

reach(inst, 'nested').should.equal(inst);
reach(inst, 'x.y').should.equal(inst);
reach(inst, 'nested')
.resolve({})
.should.equal(inst);
reach(inst, 'x.y')
.resolve({})
.should.equal(inst);
});

it('should be passed the value', done => {
Expand Down
57 changes: 39 additions & 18 deletions test/yup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import reach, { getIn } from '../src/util/reach';
import prependDeep from '../src/util/prependDeep';
import { settled } from '../src/util/runValidations';

import { object, array, string, lazy, number } from '../src';
import { object, array, string, lazy, number, ValidationError } from '../src';

describe('Yup', function() {
it('cast should not assert on undefined', () => {
Expand Down Expand Up @@ -108,8 +108,8 @@ describe('Yup', function() {
valid.should.equal(true);
});

it('should REACH conditionally correctly', function() {
var num = number(),
it('should REACH conditionally correctly', async function() {
var num = number().oneOf([4]),
inst = object().shape({
num: number().max(4),
nested: object().shape({
Expand All @@ -136,21 +136,42 @@ describe('Yup', function() {
},
};

reach(inst, 'nested.arr.num', value).should.equal(num);
reach(inst, 'nested.arr[].num', value).should.equal(num);

reach(inst, 'nested.arr.num', value, context).should.equal(num);
reach(inst, 'nested.arr[].num', value, context).should.equal(num);
reach(inst, 'nested.arr[0].num', value, context).should.equal(num);

// should fail b/c item[1] is used to resolve the schema
reach(inst, 'nested["arr"][1].num', value, context).should.not.equal(num);

return reach(inst, 'nested.arr[].num', value, context)
.isValid(5)
.then(valid => {
valid.should.equal(true);
});
let options = {};
options.parent = value.nested.arr[0];
options.value = options.parent.num;
reach(inst, 'nested.arr.num', value)
.resolve(options)
.should.equal(num);
reach(inst, 'nested.arr[].num', value)
.resolve(options)
.should.equal(num);

options.context = context;
reach(inst, 'nested.arr.num', value, context)
.resolve(options)
.should.equal(num);
reach(inst, 'nested.arr[].num', value, context)
.resolve(options)
.should.equal(num);
reach(inst, 'nested.arr[0].num', value, context)
.resolve(options)
.should.equal(num);

// // should fail b/c item[1] is used to resolve the schema
options.parent = value.nested.arr[1];
options.value = options.parent.num;
reach(inst, 'nested["arr"][1].num', value, context)
.resolve(options)
.should.not.equal(num);

let reached = reach(inst, 'nested.arr[].num', value, context);

await reached.validate(5, { context, parent: { foo: 4 } }).should.be
.fulfilled;

await reached
.validate(5, { context, parent: { foo: 5 } })
.should.be.rejectedWith(ValidationError, /one of the following/);
});

it('should reach through lazy', async () => {
Expand Down

0 comments on commit afc5119

Please sign in to comment.