Skip to content

Commit 1274a45

Browse files
committed
[changed] required() to non-exclusive
reimplement string && array required() without using min, fixes #24
1 parent 66826e7 commit 1274a45

10 files changed

+174
-75
lines changed

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,13 @@ when `false` the validations will stack. e.g. `max` is an exclusive validation,
385385
whereas the string `matches` is not. This is helpful for "toggling" validations on and off.
386386
- `useCallback`: boolean (default `false`), use the callback interface for asynchrony instead of promises
387387

388+
In the case of mixing exclusive and non-exclusive tests the following logic is used.
389+
If a non-exclusive test is added to a schema with an exclusive test of the same name
390+
the exclusive test is removed and further tests of the same name will be stacked.
391+
392+
If an exclusive test is added to a schema with non-exclusive tests of the same name
393+
the previous tests are removed and further tests of the same name will replace each other.
394+
388395
```javascript
389396
var schema = yup.mixed().test({
390397
name: 'max',

src/array.js

+17-11
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
'use strict';
22
var MixedSchema = require('./mixed')
33
, Promise = require('promise/lib/es6-extensions')
4+
, isAbsent = require('./util/isAbsent')
45
, { mixed, array: locale } = require('./locale.js')
56
, { inherits, collectErrors } = require('./util/_');
67

78
let scopeError = value => err => {
8-
err.value = value
9-
throw err
10-
}
9+
err.value = value
10+
throw err
11+
}
12+
13+
let hasLength = value => !isAbsent(value) && value.length > 0;
1114

1215
module.exports = ArraySchema
1316

@@ -49,20 +52,19 @@ inherits(ArraySchema, MixedSchema, {
4952
endEarly = schema._option('abortEarly', _opts)
5053
recursive = schema._option('recursive', _opts)
5154

52-
5355
return MixedSchema.prototype._validate.call(this, _value, _opts, _state)
5456
.catch(endEarly ? null : err => {
5557
errors = err
5658
return err.value
5759
})
5860
.then(function(value){
59-
if ( !recursive || !subType || !schema._typeCheck(value) ) {
60-
if ( errors.length ) throw errors[0]
61+
if (!recursive || !subType || !schema._typeCheck(value) ) {
62+
if (errors.length) throw errors[0]
6163
return value
6264
}
6365

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

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

88-
return next.min(1, msg || mixed.required);
90+
return next.test(
91+
'required'
92+
, msg || mixed.required
93+
, hasLength
94+
)
8995
},
9096

9197
min(min, message){
@@ -96,7 +102,7 @@ inherits(ArraySchema, MixedSchema, {
96102
name: 'min',
97103
exclusive: true,
98104
params: { min },
99-
test: value => value && value.length >= min
105+
test: value => isAbsent(value) || value.length >= min
100106
})
101107
},
102108

@@ -107,7 +113,7 @@ inherits(ArraySchema, MixedSchema, {
107113
name: 'max',
108114
exclusive: true,
109115
params: { max },
110-
test: value => value && value.length <= max
116+
test: value => isAbsent(value) || value.length <= max
111117
})
112118
},
113119

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

119125
return this.transform(values => values != null ? values.filter(reject) : values)
120126
}
121-
})
127+
})

src/date.js

+11-10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
var MixedSchema = require('./mixed')
33
, isoParse = require('./util/isodate')
44
, locale = require('./locale.js').date
5+
, isAbsent = require('./util/isAbsent')
56
, { isDate, inherits } = require('./util/_');
67

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

37-
return this.test({
38-
name: 'min',
39-
exclusive: true,
40-
message: msg || locale.min,
38+
return this.test({
39+
name: 'min',
40+
exclusive: true,
41+
message: msg || locale.min,
4142
params: { min: min },
42-
test: value => value && (value >= limit)
43+
test: value => isAbsent(value) || (value >= limit)
4344
})
4445
},
4546

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

52-
return this.test({
53+
return this.test({
5354
name: 'max',
54-
exclusive: true,
55-
message: msg || locale.max,
55+
exclusive: true,
56+
message: msg || locale.max,
5657
params: { max: max },
57-
test: value => !value || (value <= limit)
58+
test: value => isAbsent(value) || (value <= limit)
5859
})
5960
}
6061

61-
})
62+
})

src/mixed.js

+49-23
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ var Promise = require('promise/lib/es6-extensions')
55
, ValidationError = require('./util/validation-error')
66
, locale = require('./locale.js').mixed
77
, _ = require('./util/_')
8+
, isAbsent = require('./util/isAbsent')
89
, cloneDeep = require('./util/clone')
910
, createValidation = require('./util/createValidation')
1011
, BadSet = require('./util/set');
1112

1213
let formatError = ValidationError.formatError
1314

15+
let notEmpty = value => !isAbsent(value);
16+
1417
module.exports = SchemaType
1518

1619
function SchemaType(options = {}){
@@ -58,16 +61,21 @@ SchemaType.prototype = {
5861

5962
if (schema._type !== this._type && this._type !== 'mixed')
6063
throw new TypeError(`You cannot \`concat()\` schema's of different types: ${this._type} and ${schema._type}`)
61-
64+
var cloned = this.clone()
6265
var next = _.merge(this.clone(), schema.clone())
6366

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

68-
// trim exclusive tests, take the most recent ones
69-
next.tests = _.uniq(next.tests.reverse(),
70-
(fn, idx) => next[fn.VALIDATION_KEY] ? fn.VALIDATION_KEY : idx).reverse()
71+
next.tests = cloned.tests;
72+
next._exclusive = cloned._exclusive;
73+
74+
// manually add the new tests to ensure
75+
// the deduping logic is consistent
76+
schema.tests.forEach((fn) => {
77+
next = next.test(fn.TEST)
78+
});
7179

7280
next._type = schema._type;
7381

@@ -197,12 +205,11 @@ SchemaType.prototype = {
197205
},
198206

199207
required(msg) {
200-
return this.test({
201-
name: 'required',
202-
exclusive: true,
203-
message: msg || locale.required,
204-
test: value => value != null
205-
})
208+
return this.test(
209+
'required',
210+
msg || locale.required,
211+
notEmpty
212+
)
206213
},
207214

208215
typeError(msg){
@@ -223,10 +230,22 @@ SchemaType.prototype = {
223230
return next
224231
},
225232

233+
/**
234+
* Adds a test function to the schema's queue of tests.
235+
* tests can be exclusive or non-exclusive.
236+
*
237+
* - exclusive tests, will replace any existing tests of the same name.
238+
* - non-exclusive: can be stacked
239+
*
240+
* If a non-exclusive test is added to a schema with an exclusive test of the same name
241+
* the exclusive test is removed and further tests of the same name will be stacked.
242+
*
243+
* If an exclusive test is added to a schema with non-exclusive tests of the same name
244+
* the previous tests are removed and further tests of the same name will replace each other.
245+
*/
226246
test(name, message, test, useCallback) {
227247
var opts = name
228-
, next = this.clone()
229-
, isExclusive;
248+
, next = this.clone();
230249

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

244-
var validate = createValidation(opts)
245-
246-
isExclusive = opts.name && next._exclusive[opts.name] === true
263+
var validate = createValidation(opts);
247264

248-
if (opts.exclusive || isExclusive) {
249-
if (!opts.name)
250-
throw new TypeError('You cannot have an exclusive validation without a `name`')
265+
var isExclusive = (
266+
opts.exclusive ||
267+
(opts.name && next._exclusive[opts.name] === true)
268+
)
251269

252-
next._exclusive[opts.name] = true
253-
validate.VALIDATION_KEY = opts.name
270+
if (opts.exclusive && !opts.name) {
271+
throw new TypeError('You cannot have an exclusive validation without a `name`')
254272
}
255273

256-
if (isExclusive)
257-
next.tests = next.tests.filter(fn => fn.VALIDATION_KEY !== opts.name)
274+
next._exclusive[opts.name] = !!opts.exclusive
275+
276+
next.tests = next.tests
277+
.filter(fn => {
278+
if (fn.TEST_NAME === opts.name) {
279+
if (isExclusive) return false
280+
if (fn.TEST.test === validate.TEST.test) return false
281+
}
282+
return true
283+
})
258284

259285
next.tests.push(validate)
260286

@@ -314,7 +340,7 @@ var aliases = {
314340
}
315341

316342

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

src/number.js

+17-14
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
'use strict';
22
var SchemaObject = require('./mixed')
33
, locale = require('./locale.js').number
4+
, isAbsent = require('./util/isAbsent')
45
, { isDate, inherits } = require('./util/_');
56

67
module.exports = NumberSchema
78

9+
let isInteger = val => isAbsent(val) || val === (val | 0)
10+
811
function NumberSchema(){
9-
if ( !(this instanceof NumberSchema))
12+
if ( !(this instanceof NumberSchema))
1013
return new NumberSchema()
1114

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

3134
min(min, msg) {
32-
return this.test({
33-
name: 'min',
34-
exclusive: true,
35-
params: { min },
35+
return this.test({
36+
name: 'min',
37+
exclusive: true,
38+
params: { min },
3639
message: msg || locale.min,
37-
test: value => value == null || value >= min
40+
test: value => isAbsent(value) || value >= min
3841
})
3942
},
4043

4144
max(max, msg) {
42-
return this.test({
43-
name: 'max',
44-
exclusive: true,
45-
params: { max },
45+
return this.test({
46+
name: 'max',
47+
exclusive: true,
48+
params: { max },
4649
message: msg || locale.max,
47-
test: value => value == null || value <= max
50+
test: value => isAbsent(value) || value <= max
4851
})
4952
},
5053

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

6265
return this
63-
.transform( v => v != null ? (v | 0) : v)
64-
.test('integer', msg, val => val == null || val === (val | 0))
66+
.transform(value => !isAbsent(value) ? (value | 0) : value)
67+
.test('integer', msg, isInteger)
6568
},
6669

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

74-
return this.transform(v => v != null ? Math[method](v) : v)
77+
return this.transform(value => !isAbsent(value) ? Math[method](value) : value)
7578
}
7679
})

0 commit comments

Comments
 (0)