diff --git a/src/Condition.js b/src/Condition.js index 9d03437cf..a5d9024c8 100644 --- a/src/Condition.js +++ b/src/Condition.js @@ -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; diff --git a/src/Lazy.js b/src/Lazy.js index d09266745..0de633aa4 100644 --- a/src/Lazy.js +++ b/src/Lazy.js @@ -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); diff --git a/src/mixed.js b/src/mixed.js index 0f6c376ea..1ca760265 100644 --- a/src/mixed.js +++ b/src/mixed.js @@ -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 = {}) { diff --git a/src/util/reach.js b/src/util/reach.js index d033d1978..1f8fae725 100644 --- a/src/util/reach.js +++ b/src/util/reach.js @@ -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) => { @@ -55,10 +55,6 @@ export function getIn(schema, path, value, context) { } }); - if (schema) { - schema = schema.resolve({ context, parent, value }); - } - return { schema, parent, parentPath: lastPart }; } diff --git a/test/mixed.js b/test/mixed.js index 2487652e2..17145fc5c 100644 --- a/test/mixed.js +++ b/test/mixed.js @@ -7,6 +7,7 @@ import { ref, reach, bool, + lazy, ValidationError, } from '../src'; @@ -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({ diff --git a/test/object.js b/test/object.js index 2069d5122..705067fb0 100644 --- a/test/object.js +++ b/test/object.js @@ -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 => { diff --git a/test/yup.js b/test/yup.js index 9ec8d51ac..e87734585 100644 --- a/test/yup.js +++ b/test/yup.js @@ -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', () => { @@ -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({ @@ -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 () => {