Skip to content

Commit

Permalink
[changed] required() to non-exclusive
Browse files Browse the repository at this point in the history
reimplement string && array required() without using min, fixes jquense#24
  • Loading branch information
jquense committed Feb 1, 2016
1 parent 66826e7 commit 1274a45
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 75 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,13 @@ when `false` the validations will stack. e.g. `max` is an exclusive validation,
whereas the string `matches` is not. This is helpful for "toggling" validations on and off.
- `useCallback`: boolean (default `false`), use the callback interface for asynchrony instead of promises

In the case of mixing exclusive and non-exclusive tests the following logic is used.
If a non-exclusive test is added to a schema with an exclusive test of the same name
the exclusive test is removed and further tests of the same name will be stacked.

If an exclusive test is added to a schema with non-exclusive tests of the same name
the previous tests are removed and further tests of the same name will replace each other.

```javascript
var schema = yup.mixed().test({
name: 'max',
Expand Down
28 changes: 17 additions & 11 deletions src/array.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
'use strict';
var MixedSchema = require('./mixed')
, Promise = require('promise/lib/es6-extensions')
, isAbsent = require('./util/isAbsent')
, { mixed, array: locale } = require('./locale.js')
, { inherits, collectErrors } = require('./util/_');

let scopeError = value => err => {
err.value = value
throw err
}
err.value = value
throw err
}

let hasLength = value => !isAbsent(value) && value.length > 0;

module.exports = ArraySchema

Expand Down Expand Up @@ -49,20 +52,19 @@ inherits(ArraySchema, MixedSchema, {
endEarly = schema._option('abortEarly', _opts)
recursive = schema._option('recursive', _opts)


return MixedSchema.prototype._validate.call(this, _value, _opts, _state)
.catch(endEarly ? null : err => {
errors = err
return err.value
})
.then(function(value){
if ( !recursive || !subType || !schema._typeCheck(value) ) {
if ( errors.length ) throw errors[0]
if (!recursive || !subType || !schema._typeCheck(value) ) {
if (errors.length) throw errors[0]
return value
}

let result = value.map((item, key) => {
var path = (_state.path || '') + '['+ key + ']'
var path = (_state.path || '') + '[' + key + ']'
, state = { ..._state, path, key, parent: value};

return subType._validate(item, _opts, state)
Expand All @@ -85,7 +87,11 @@ inherits(ArraySchema, MixedSchema, {
required(msg) {
var next = MixedSchema.prototype.required.call(this, msg || mixed.required);

return next.min(1, msg || mixed.required);
return next.test(
'required'
, msg || mixed.required
, hasLength
)
},

min(min, message){
Expand All @@ -96,7 +102,7 @@ inherits(ArraySchema, MixedSchema, {
name: 'min',
exclusive: true,
params: { min },
test: value => value && value.length >= min
test: value => isAbsent(value) || value.length >= min
})
},

Expand All @@ -107,7 +113,7 @@ inherits(ArraySchema, MixedSchema, {
name: 'max',
exclusive: true,
params: { max },
test: value => value && value.length <= max
test: value => isAbsent(value) || value.length <= max
})
},

Expand All @@ -118,4 +124,4 @@ inherits(ArraySchema, MixedSchema, {

return this.transform(values => values != null ? values.filter(reject) : values)
}
})
})
21 changes: 11 additions & 10 deletions src/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
var MixedSchema = require('./mixed')
, isoParse = require('./util/isodate')
, locale = require('./locale.js').date
, isAbsent = require('./util/isAbsent')
, { isDate, inherits } = require('./util/_');

let invalidDate = new Date('')
Expand Down Expand Up @@ -34,12 +35,12 @@ inherits(DateSchema, MixedSchema, {
if(!this._typeCheck(limit))
throw new TypeError('`min` must be a Date or a value that can be `cast()` to a Date')

return this.test({
name: 'min',
exclusive: true,
message: msg || locale.min,
return this.test({
name: 'min',
exclusive: true,
message: msg || locale.min,
params: { min: min },
test: value => value && (value >= limit)
test: value => isAbsent(value) || (value >= limit)
})
},

Expand All @@ -49,13 +50,13 @@ inherits(DateSchema, MixedSchema, {
if(!this._typeCheck(limit))
throw new TypeError('`max` must be a Date or a value that can be `cast()` to a Date')

return this.test({
return this.test({
name: 'max',
exclusive: true,
message: msg || locale.max,
exclusive: true,
message: msg || locale.max,
params: { max: max },
test: value => !value || (value <= limit)
test: value => isAbsent(value) || (value <= limit)
})
}

})
})
72 changes: 49 additions & 23 deletions src/mixed.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ var Promise = require('promise/lib/es6-extensions')
, ValidationError = require('./util/validation-error')
, locale = require('./locale.js').mixed
, _ = require('./util/_')
, isAbsent = require('./util/isAbsent')
, cloneDeep = require('./util/clone')
, createValidation = require('./util/createValidation')
, BadSet = require('./util/set');

let formatError = ValidationError.formatError

let notEmpty = value => !isAbsent(value);

module.exports = SchemaType

function SchemaType(options = {}){
Expand Down Expand Up @@ -58,16 +61,21 @@ SchemaType.prototype = {

if (schema._type !== this._type && this._type !== 'mixed')
throw new TypeError(`You cannot \`concat()\` schema's of different types: ${this._type} and ${schema._type}`)

var cloned = this.clone()
var next = _.merge(this.clone(), schema.clone())

// undefined isn't merged over, but is a valid value for default
if (schema._default === undefined && _.has(this, '_default'))
next._default = schema._default

// trim exclusive tests, take the most recent ones
next.tests = _.uniq(next.tests.reverse(),
(fn, idx) => next[fn.VALIDATION_KEY] ? fn.VALIDATION_KEY : idx).reverse()
next.tests = cloned.tests;
next._exclusive = cloned._exclusive;

// manually add the new tests to ensure
// the deduping logic is consistent
schema.tests.forEach((fn) => {
next = next.test(fn.TEST)
});

next._type = schema._type;

Expand Down Expand Up @@ -197,12 +205,11 @@ SchemaType.prototype = {
},

required(msg) {
return this.test({
name: 'required',
exclusive: true,
message: msg || locale.required,
test: value => value != null
})
return this.test(
'required',
msg || locale.required,
notEmpty
)
},

typeError(msg){
Expand All @@ -223,10 +230,22 @@ SchemaType.prototype = {
return next
},

/**
* Adds a test function to the schema's queue of tests.
* tests can be exclusive or non-exclusive.
*
* - exclusive tests, will replace any existing tests of the same name.
* - non-exclusive: can be stacked
*
* If a non-exclusive test is added to a schema with an exclusive test of the same name
* the exclusive test is removed and further tests of the same name will be stacked.
*
* If an exclusive test is added to a schema with non-exclusive tests of the same name
* the previous tests are removed and further tests of the same name will replace each other.
*/
test(name, message, test, useCallback) {
var opts = name
, next = this.clone()
, isExclusive;
, next = this.clone();

if (typeof name === 'string') {
if (typeof message === 'function')
Expand All @@ -241,20 +260,27 @@ SchemaType.prototype = {
if (next._whitelist.length)
throw new Error('Cannot add tests when specific valid values are specified')

var validate = createValidation(opts)

isExclusive = opts.name && next._exclusive[opts.name] === true
var validate = createValidation(opts);

if (opts.exclusive || isExclusive) {
if (!opts.name)
throw new TypeError('You cannot have an exclusive validation without a `name`')
var isExclusive = (
opts.exclusive ||
(opts.name && next._exclusive[opts.name] === true)
)

next._exclusive[opts.name] = true
validate.VALIDATION_KEY = opts.name
if (opts.exclusive && !opts.name) {
throw new TypeError('You cannot have an exclusive validation without a `name`')
}

if (isExclusive)
next.tests = next.tests.filter(fn => fn.VALIDATION_KEY !== opts.name)
next._exclusive[opts.name] = !!opts.exclusive

next.tests = next.tests
.filter(fn => {
if (fn.TEST_NAME === opts.name) {
if (isExclusive) return false
if (fn.TEST.test === validate.TEST.test) return false
}
return true
})

next.tests.push(validate)

Expand Down Expand Up @@ -314,7 +340,7 @@ var aliases = {
}


for( var method in aliases ) if ( _.has(aliases, method) )
for (var method in aliases) if ( _.has(aliases, method) )
aliases[method].forEach(
alias => SchemaType.prototype[alias] = SchemaType.prototype[method]) //eslint-disable-line no-loop-func

Expand Down
31 changes: 17 additions & 14 deletions src/number.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
'use strict';
var SchemaObject = require('./mixed')
, locale = require('./locale.js').number
, isAbsent = require('./util/isAbsent')
, { isDate, inherits } = require('./util/_');

module.exports = NumberSchema

let isInteger = val => isAbsent(val) || val === (val | 0)

function NumberSchema(){
if ( !(this instanceof NumberSchema))
if ( !(this instanceof NumberSchema))
return new NumberSchema()

SchemaObject.call(this, { type: 'number' })
Expand All @@ -29,22 +32,22 @@ inherits(NumberSchema, SchemaObject, {
},

min(min, msg) {
return this.test({
name: 'min',
exclusive: true,
params: { min },
return this.test({
name: 'min',
exclusive: true,
params: { min },
message: msg || locale.min,
test: value => value == null || value >= min
test: value => isAbsent(value) || value >= min
})
},

max(max, msg) {
return this.test({
name: 'max',
exclusive: true,
params: { max },
return this.test({
name: 'max',
exclusive: true,
params: { max },
message: msg || locale.max,
test: value => value == null || value <= max
test: value => isAbsent(value) || value <= max
})
},

Expand All @@ -60,8 +63,8 @@ inherits(NumberSchema, SchemaObject, {
msg = msg || locale.integer

return this
.transform( v => v != null ? (v | 0) : v)
.test('integer', msg, val => val == null || val === (val | 0))
.transform(value => !isAbsent(value) ? (value | 0) : value)
.test('integer', msg, isInteger)
},

round(method) {
Expand All @@ -71,6 +74,6 @@ inherits(NumberSchema, SchemaObject, {
if( avail.indexOf(method.toLowerCase()) === -1 )
throw new TypeError('Only valid options for round() are: ' + avail.join(', '))

return this.transform(v => v != null ? Math[method](v) : v)
return this.transform(value => !isAbsent(value) ? Math[method](value) : value)
}
})
Loading

0 comments on commit 1274a45

Please sign in to comment.