From f2b007889925b872413aa28522926685d2c2b59e Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Fri, 24 Jun 2016 11:16:36 -0400 Subject: [PATCH] [changed] Less aggressive type coercions Type casts no longer "succeed without fail". For instance `boolean` will throw if a cast produces an invalid type, instead of quietly coercing to `false`. By default `cast` will now throw in these situations, passing `assert: false` to cast options will disable this behavior and the value returned will be the invalid value (NaN, InvalidDate, null) or the original value if no good invalid value exists in the language ``` number().cast('foo', { assert: false }) // -> NaN bool().cast('foo', { assert: false }) // -> 'foo' ``` --- .eslintrc | 9 +- README.md | 48 ++++- package.json | 2 + src/{util/condition.js => Condition.js} | 13 +- src/{util/lazy.js => Lazy.js} | 2 +- src/{util/reference.js => Reference.js} | 8 +- ...validation-error.js => ValidationError.js} | 4 +- src/array.js | 67 ++++--- src/boolean.js | 20 +- src/date.js | 17 +- src/index.js | 16 +- src/locale.js | 97 +++++----- src/mixed.js | 93 +++++---- src/number.js | 31 +-- src/object.js | 183 ++++++++---------- src/string.js | 28 +-- src/util/_.js | 127 ------------ src/util/clone.js | 7 +- src/util/createValidation.js | 14 +- src/util/inherits.js | 13 ++ src/util/isSchema.js | 3 + src/util/isodate.js | 12 +- src/util/merge.js | 34 ++++ src/util/reach.js | 4 +- src/util/runValidations.js | 69 +++++++ src/util/scopeToValue.js | 16 ++ src/util/set.js | 2 +- .../{sortByFields.js => sortByKeyOrder.js} | 5 +- src/util/sortFields.js | 34 ++++ test/array.js | 29 ++- test/bool.js | 15 +- test/date.js | 23 +-- test/helpers.js | 46 +++++ test/mixed.js | 36 ++-- test/number.js | 176 +++++++++++------ test/object.js | 49 +++-- test/string.js | 97 ++++++---- test/yup.js | 31 +-- tests-webpack.js | 2 + 39 files changed, 819 insertions(+), 663 deletions(-) rename src/{util/condition.js => Condition.js} (81%) rename src/{util/lazy.js => Lazy.js} (93%) rename src/{util/reference.js => Reference.js} (88%) rename src/{util/validation-error.js => ValidationError.js} (96%) delete mode 100644 src/util/_.js create mode 100644 src/util/inherits.js create mode 100644 src/util/isSchema.js create mode 100644 src/util/merge.js create mode 100644 src/util/runValidations.js create mode 100644 src/util/scopeToValue.js rename src/util/{sortByFields.js => sortByKeyOrder.js} (74%) create mode 100644 src/util/sortFields.js create mode 100644 test/helpers.js diff --git a/.eslintrc b/.eslintrc index 0d3777748..f3d348f04 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,21 +3,24 @@ "extends": "eslint:recommended", "globals": { "sinon": true, - "expect": true + "expect": true, + "TestHelpers": true, }, "env": { "browser": true, "node": true, - "mocha": true + "mocha": true, }, "rules": { + "eqeqeq": 0, + "no-loop-func": 0, + "comma-dangle": 0, "no-eval": 2, "strict": 0, "eol-last": 0, "dot-notation": [2, { "allowKeywords": true }], "semi": [0, "never"], "curly": 0, - "eqeqeq": [2, "allow-null"], "no-undef": 2, "quotes": [2, "single", "avoid-escape"], "no-trailing-spaces": 0, diff --git a/README.md b/README.md index eb3ede62c..4f15515a7 100644 --- a/README.md +++ b/README.md @@ -633,7 +633,7 @@ transforms are run before validations and only applied when `strict` is `true`. Transformations are useful for arbitrarily altering how the object is cast, __however, you should take care not to mutate the passed in value.__ Transforms are run sequentially so each `value` represents the -current state of the cast, you can use the `orignalValue` param if you need to work on the raw initial value. +current state of the cast, you can use the `originalValue` param if you need to work on the raw initial value. ```javascript var schema = yup.string().transform(function(currentValue, originalvalue){ @@ -671,6 +671,11 @@ var schema = yup.string(); schema.isValid('hello') //=> true ``` +By default, the `cast` logic of `string` is to call `toString` on the value if it exists. +empty values are not coerced (use `ensure()` to coerce empty values to empty strings). + +Failed casts return the input value. + #### `string.required(message: ?string): Schema` The same as the `mixed()` schema required, except that empty strings are also considered 'missing' values. @@ -702,6 +707,10 @@ Validates the value as an email address via a regex. Validates the value as a valid URL via a regex. +#### `string.ensure(): Schema` + +Transforms `undefined` and `null` values to an empty string along with +setting the `default` to an empty string. #### `string.trim(message: ?string): Schema` @@ -710,11 +719,13 @@ Transforms string values by removing leading and trailing whitespace. If #### `string.lowercase(message: ?string): Schema` -Transforms the string value to lowercase. If `strict()` is set it will only validate that the value is lowercase. +Transforms the string value to lowercase. If `strict()` is set it +will only validate that the value is lowercase. #### `string.uppercase(message: ?string): Schema` -Transforms the string value to uppercase. If `strict()` is set it will only validate that the value is uppercase. +Transforms the string value to uppercase. If `strict()` is set it +will only validate that the value is uppercase. ### number @@ -725,6 +736,10 @@ var schema = yup.number(); schema.isValid(10) //=> true ``` +The default `cast` logic of `number` is: [`parseFloat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat). + +Failed casts return `NaN`. + #### `number.min(limit: number | Ref, message: ?string): Schema` Set the minimum value allowed. The `${min}` interpolation can be used in the @@ -745,13 +760,16 @@ Value must be a negative number. #### `number.integer(message: ?string): Schema` -Transformation that coerces the value into an integer via truncation -` value | 0`. If `strict()` is set it will only validate that the value is an integer. +Validates that a number is an integer. + +#### `number.truncate(): Schema` -#### `number.round(type: 'floor' | 'ceil' | 'round' = 'round'): Schema` +Transformation that coerces the value to an integer by stripping off the digits +to the right of the decimal point. -Rounds the value by the specified method (defaults to 'round'). +#### `number.round(type: 'floor' | 'ceil' | 'trunc' | 'round' = 'round'): Schema` +Adjusts the value via the specified method of `Math` (defaults to 'round'). ### boolean @@ -773,6 +791,12 @@ var schema = yup.date(); schema.isValid(new Date) //=> true ``` +The default `cast` logic of `date` is pass the value to the `Date` constructor, failing that, it will attempt +to parse the date as an ISO date string. + +Failed casts return an invalid Date. + + #### `date.min(limit: Date | string | Ref, message: ?string): Schema` Set the minimum date allowed. When a string is provided it will attempt to cast to a date first @@ -805,6 +829,10 @@ array().of(number()) array(number()) ``` +The default `cast` behavior for `array` is: [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) + +Failed casts return: `null`; + #### `array.of(type: Schema): Schema` Specify the schema of array elements. `of()` is optional and when omitted the array schema will @@ -876,6 +904,10 @@ object({ }) ``` +The default `cast` behavior for `object` is: [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) + +Failed casts return: `null`; + #### `object.shape(fields: object, noSortEdges: ?Array<[string, string]>): Schema` @@ -909,6 +941,8 @@ Transforms all object keys to camelCase Transforms all object keys to CONSTANT_CASE. + + ## Extending Schema Types The simplest way to extend an existing type is just to cache a configured schema and use that through your application. diff --git a/package.json b/package.json index 2b398b878..0f3f802b2 100644 --- a/package.json +++ b/package.json @@ -59,8 +59,10 @@ "dependencies": { "case": "^1.2.1", "fn-name": "~1.0.1", + "lodash": "^4.13.1", "property-expr": "^1.2.0", "toposort": "^0.2.10", + "type-name": "^2.0.1", "universal-promise": "^1.0.1" }, "release-script": { diff --git a/src/util/condition.js b/src/Condition.js similarity index 81% rename from src/util/condition.js rename to src/Condition.js index de701a95a..60a147e5e 100644 --- a/src/util/condition.js +++ b/src/Condition.js @@ -1,7 +1,5 @@ -'use strict'; -var { transform, has, isSchema } = require('./_') - -module.exports = Conditional +import has from 'lodash/has'; +import isSchema from './util/isSchema'; class Conditional { @@ -18,8 +16,9 @@ class Conditional { throw new TypeError('`is:` is required for `when()` conditions') if (!options.then && !options.otherwise) - throw new TypeError('either `then:` or `otherwise:` is required for `when()` conditions') - + throw new TypeError( + 'either `then:` or `otherwise:` is required for `when()` conditions' + ) let isFn = typeof is === 'function' ? is : ((...values) => values.every(value => value === is)) @@ -47,4 +46,4 @@ class Conditional { } } -module.exports = Conditional; +export default Conditional; diff --git a/src/util/lazy.js b/src/Lazy.js similarity index 93% rename from src/util/lazy.js rename to src/Lazy.js index d9ed062d0..8b4fd6397 100644 --- a/src/util/lazy.js +++ b/src/Lazy.js @@ -1,4 +1,4 @@ -var { isSchema } = require('./_') +import isSchema from './util/isSchema'; class Lazy { constructor(mapFn) { diff --git a/src/util/reference.js b/src/Reference.js similarity index 88% rename from src/util/reference.js rename to src/Reference.js index 2c3394d12..6db001c07 100644 --- a/src/util/reference.js +++ b/src/Reference.js @@ -1,13 +1,13 @@ -var getter = require('property-expr').getter +import { getter } from 'property-expr'; let validateName = d => { if (typeof d !== 'string') throw new TypeError('ref\'s must be strings, got: ' + d) } -export default class Ref { +export default class Reference { static isRef(value) { - return !!(value && (value.__isYupRef || value instanceof Ref)) + return !!(value && (value.__isYupRef || value instanceof Reference)) } constructor(key, mapFn, options = {}) { @@ -40,4 +40,4 @@ export default class Ref { } } -Ref.prototype.__isYupRef = true +Reference.prototype.__isYupRef = true diff --git a/src/util/validation-error.js b/src/ValidationError.js similarity index 96% rename from src/util/validation-error.js rename to src/ValidationError.js index f4972c554..933f0266c 100644 --- a/src/util/validation-error.js +++ b/src/ValidationError.js @@ -1,5 +1,5 @@ -'use strict'; -var strReg = /\$\{\s*(\w+)\s*\}/g; + +let strReg = /\$\{\s*(\w+)\s*\}/g; let replace = str => params => str.replace(strReg, (_, key) => params[key] || '') diff --git a/src/array.js b/src/array.js index 785675cb3..7aa0600c0 100644 --- a/src/array.js +++ b/src/array.js @@ -1,18 +1,13 @@ -'use strict'; -var MixedSchema = require('./mixed') - , Promise = require('universal-promise') - , isAbsent = require('./util/isAbsent') - , { mixed, array: locale } = require('./locale.js') - , { inherits, collectErrors } = require('./util/_'); - -let scopeError = value => err => { - err.value = value - throw err -} +import inherits from './util/inherits'; +import isAbsent from './util/isAbsent'; +import MixedSchema from './mixed'; +import { mixed, array as locale } from './locale.js'; +import runValidations, { propagateErrors } from './util/runValidations'; + let hasLength = value => !isAbsent(value) && value.length > 0; -module.exports = ArraySchema +export default ArraySchema function ArraySchema(type) { if (!(this instanceof ArraySchema)) @@ -54,29 +49,31 @@ inherits(ArraySchema, MixedSchema, { }, _validate(_value, options = {}) { - var errors = [] - , subType, endEarly, recursive; - - subType = this._subType - endEarly = this._option('abortEarly', options) - recursive = this._option('recursive', options) - - return MixedSchema.prototype._validate.call(this, _value, options) - .catch(endEarly ? null : err => { - errors = err - return err.value - }) + let errors = [] + let path = options.path + let subType = this._subType + let endEarly = this._option('abortEarly', options) + let recursive = this._option('recursive', options) + + return MixedSchema.prototype._validate + .call(this, _value, options) + .catch(propagateErrors(endEarly, errors)) .then((value) => { if (!recursive || !subType || !this._typeCheck(value) ) { if (errors.length) throw errors[0] return value } - let validations = value.map((item, key) => { - var path = (options.path || '') + '[' + key + ']' + let validations = value.map((item, idx) => { + var path = (options.path || '') + '[' + idx + ']' // object._validate note for isStrict explanation - var innerOptions = { ...options, path, key, strict: true, parent: value }; + var innerOptions = { + ...options, + path, + strict: true, + parent: value + }; if (subType.validate) return subType.validate(item, innerOptions) @@ -84,11 +81,13 @@ inherits(ArraySchema, MixedSchema, { return true }) - validations = endEarly - ? Promise.all(validations).catch(scopeError(value)) - : collectErrors({ validations, value, errors, path: options.path }) - - return validations.then(() => value) + return runValidations({ + path, + value, + errors, + endEarly, + validations + }) }) }, @@ -137,8 +136,8 @@ inherits(ArraySchema, MixedSchema, { ensure() { return this - .default([]) - .transform(val => val == null ? [] : [].concat(val)) + .default(() => []) + .transform(val => val === null ? [] : [].concat(val)) }, compact(rejector){ diff --git a/src/boolean.js b/src/boolean.js index d117da104..638636a3f 100644 --- a/src/boolean.js +++ b/src/boolean.js @@ -1,8 +1,7 @@ -'use strict'; -var MixedSchema = require('./mixed') - , inherits = require('./util/_').inherits; +import inherits from './util/inherits'; +import MixedSchema from './mixed'; -module.exports = BooleanSchema +export default BooleanSchema function BooleanSchema(){ if (!(this instanceof BooleanSchema)) @@ -12,15 +11,20 @@ function BooleanSchema(){ this.withMutation(() => { this.transform(function(value) { - if ( this.isType(value) ) return value - return (/true|1/i).test(value) + if (!this.isType(value)) { + if (/^(true|1)$/i.test(value)) return true + if (/^(false|0)$/i.test(value)) return false + } + return value }) }) } inherits(BooleanSchema, MixedSchema, { - _typeCheck(v){ - return (typeof v === 'boolean') || (typeof v === 'object' && v instanceof Boolean) + _typeCheck(v) { + if (v instanceof Boolean) v = v.valueOf(); + + return typeof v === 'boolean' } }) diff --git a/src/date.js b/src/date.js index b0bac9872..d15d3b7b3 100644 --- a/src/date.js +++ b/src/date.js @@ -1,14 +1,15 @@ -'use strict'; -var MixedSchema = require('./mixed') - , isoParse = require('./util/isodate') - , locale = require('./locale.js').date - , isAbsent = require('./util/isAbsent') - , Ref = require('./util/reference') - , { isDate, inherits } = require('./util/_'); +import MixedSchema from './mixed'; +import inherits from './util/inherits'; +import isoParse from './util/isodate'; +import { date as locale } from './locale.js'; +import isAbsent from './util/isAbsent'; +import Ref from './Reference'; let invalidDate = new Date('') -module.exports = DateSchema +let isDate = obj => Object.prototype.toString.call(obj) === '[object Date]' + +export default DateSchema function DateSchema(){ if ( !(this instanceof DateSchema)) return new DateSchema() diff --git a/src/index.js b/src/index.js index e43ea5db9..f891619fb 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,10 @@ -'use strict'; -var mixed = require('./mixed') - , bool = require('./boolean') - , Ref = require('./util/reference') - , Lazy = require('./util/lazy'); +import mixed from './mixed'; +import bool from './boolean'; +import Ref from './Reference'; +import Lazy from './Lazy'; +import isSchema from './util/isSchema'; -var isSchema = schema => schema && !!schema.__isYupSchema__; - -module.exports = { +export default { mixed: mixed, string: require('./string'), number: require('./number'), @@ -18,7 +16,7 @@ module.exports = { reach: require('./util/reach'), - ValidationError: require('./util/validation-error'), + ValidationError: require('./ValidationError'), ref: (key, options) => new Ref(key, options), lazy: (fn) => new Lazy(fn), diff --git a/src/locale.js b/src/locale.js index 44a04f699..2a413618f 100644 --- a/src/locale.js +++ b/src/locale.js @@ -1,48 +1,55 @@ +export let mixed = { + default: '${path} is invalid', + notType: '${path} must be a `${type}` type, got: "${value}" instead', + required: '${path} is a required field', + oneOf: '${path} must be one the following values: ${values}', + notOneOf: '${path} must not be one the following values: ${values}' +} + +export let string = { + required: '${path} is a required field', + min: '${path} must be at least ${min} characters', + max: '${path} must be at most ${max} characters', + matches: '${path} must match the following: "${regex}"', + email: '${path} must be a valid email', + url: '${path} must be a valid URL', + trim: '${path} must be a trimmed string', + lowercase: '${path} must be a lowercase string', + uppercase: '${path} must be a upper case string' +} + +export let number = { + min: '${path} must be greater than or equal to ${min}', + max: '${path} must be less than or equal to ${max}', + positive: '${path} must be a positive number', + negative: '${path} must be a negative number', + integer: '${path} must be an integer', +}; + +export let date = { + min: '${path} field must be later than ${min}', + max: '${path} field must be at earlier than ${max}', +} + +export let boolean = {}; + +export let object = { + noUnknown: '${path} field cannot have keys not specified in the object shape', +} + +export let array = { + required: '${path} is a required field', + min: '${path} field must have at least ${min} items', + max: '${path} field must have less than ${max} items', +} -module.exports = { - mixed: { - default: '${path} is invalid', - notType: '${path} (value: `${value}`) must be a `${type}` type', - required: '${path} is a required field', - oneOf: '${path} must be one the following values: ${values}', - notOneOf: '${path} must not be one the following values: ${values}' - }, - - string: { - required: '${path} is a required field', - min: '${path} must be at least ${min} characters', - max: '${path} must be at most ${max} characters', - matches: '${path} must match the following: "${regex}"', - email: '${path} must be a valid email', - url: '${path} must be a valid URL', - trim: '${path} must be a trimmed string', - lowercase: '${path} must be a lowercase string', - uppercase: '${path} must be a upper case string' - }, - - number: { - min: '${path} must be greater than or equal to ${min}', - max: '${path} must be less than or equal to ${max}', - positive: '${path} must be a positive number', - negative: '${path} must be a negative number', - integer: '${path} must be an integer' - }, - - date: { - min: '${path} field must be later than ${min}', - max: '${path} field must be at earlier than ${max}' - }, - - boolean: {}, - - object: { - noUnknown: '${path} field cannot have keys not specified in the object shape', - }, - - array: { - required: '${path} is a required field', - min: '${path} field must have at least ${min} items', - max: '${path} field must have less than ${max} items' - } +export default { + mixed, + string, + number, + date, + object, + array, + boolean, } diff --git a/src/mixed.js b/src/mixed.js index cb9173e69..f99cdfa7d 100644 --- a/src/mixed.js +++ b/src/mixed.js @@ -1,24 +1,19 @@ -'use strict'; - -var Promise = require('universal-promise') - , Condition = require('./util/condition') - , locale = require('./locale.js').mixed - , _ = require('./util/_') - , isAbsent = require('./util/isAbsent') - , cloneDeep = require('./util/clone') - , createValidation = require('./util/createValidation') - , BadSet = require('./util/set') - , Ref = require('./util/reference'); +import typeOf from 'type-name'; +import has from 'lodash/has'; + +import { mixed as locale } from './locale'; +import Condition from './Condition'; +import runValidations from './util/runValidations'; +import merge from './util/merge'; +import isAbsent from './util/isAbsent'; +import cloneDeep from './util/clone'; +import createValidation from './util/createValidation'; +import BadSet from './util/set'; +import Ref from './Reference'; let notEmpty = value => !isAbsent(value); -function runValidations(validations, endEarly, value, path) { - return endEarly - ? Promise.all(validations) - : _.collectErrors({ validations, value, path }) -} - function extractTestParams(name, message, test, useCallback) { var opts = name; @@ -56,7 +51,7 @@ function SchemaType(options = {}){ this.typeError(locale.notType) }) - if (_.has(options, 'default')) + if (has(options, 'default')) this._defaultDefault = options.default this._type = options.type || 'mixed' @@ -104,10 +99,10 @@ 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()) + 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')) + if (schema._default === undefined && has(this, '_default')) next._default = schema._default next.tests = cloned.tests; @@ -141,7 +136,16 @@ SchemaType.prototype = { cast(value, opts = {}) { let schema = this.resolve(opts) - return schema._cast(value, opts) + let result = schema._cast(value, opts); + + if (opts.assert !== false && !this.isType(result)) { + throw new TypeError( + `Expected ${opts.path || 'field'} to be type: "${this._type}". ` + + `Got "${typeOf(value)}" instead.` + ); + } + + return result; }, _cast(_value) { @@ -149,7 +153,7 @@ SchemaType.prototype = { : this.transforms.reduce( (value, transform) => transform.call(this, value, _value), _value) - if (value === undefined && (_.has(this, '_default'))) { + if (value === undefined && (has(this, '_default'))) { value = this.default() } @@ -178,7 +182,7 @@ SchemaType.prototype = { let label = this._label if (!isStrict) { - value = this._cast(value, options, options) + value = this._cast(value, { assert: false, ...options }) } // value is cast, we can check if it meets type requirements let validationParams = { value, path, schema: this, options, label } @@ -193,14 +197,13 @@ SchemaType.prototype = { if (this._blacklistError) initialTests.push(this._blacklistError(validationParams)); - return runValidations(initialTests, endEarly, value, path) - .then(() => runValidations( - this.tests.map(fn => fn(validationParams)) - , endEarly - , value - , path - )) - .then(() => value) + return runValidations({ validations: initialTests, endEarly, value, path }) + .then(value => runValidations({ + path, + value, + endEarly, + validations: this.tests.map(fn => fn(validationParams)), + })) }, @@ -225,7 +228,7 @@ SchemaType.prototype = { default(def) { if (arguments.length === 0) { - var dflt = _.has(this, '_default') ? this._default : this._defaultDefault + var dflt = has(this, '_default') ? this._default : this._defaultDefault return typeof dflt === 'function' ? dflt.call(this) : cloneDeep(dflt) } @@ -400,7 +403,7 @@ SchemaType.prototype = { }, _option(key, overrides){ - return _.has(overrides, key) + return has(overrides, key) ? overrides[key] : this._options[key] }, @@ -417,26 +420,22 @@ SchemaType.prototype = { } -var aliases = { +let aliases = { oneOf: ['equals', 'is'], notOneOf: ['not', 'nope'] } - -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 - +Object.keys(aliases).forEach(method => { + aliases[method].forEach(alias => + SchemaType.prototype[alias] = SchemaType.prototype[method] + ) +}) function nodeify(promise, cb){ if(typeof cb !== 'function') return promise - promise.then(val => cb(null, val), err => cb(err)) + promise.then( + val => cb(null, val), + err => cb(err) + ) } - -// [{ value, exclude }] - -// values.every(({ value, exclude }) => { -// var isEql = eql(value, otherval) -// return (exclude && !isEql) || isEql -// }) diff --git a/src/number.js b/src/number.js index d6570b91c..a49a09c98 100644 --- a/src/number.js +++ b/src/number.js @@ -1,36 +1,39 @@ -'use strict'; -var SchemaObject = require('./mixed') - , locale = require('./locale.js').number - , isAbsent = require('./util/isAbsent') - , { isDate, inherits } = require('./util/_'); +import inherits from './util/inherits'; +import MixedSchema from './mixed'; +import { number as locale} from './locale.js'; +import isAbsent from './util/isAbsent'; module.exports = NumberSchema +let isNaN = value => value != +value + let isInteger = val => isAbsent(val) || val === (val | 0) -function NumberSchema(){ +function NumberSchema() { if ( !(this instanceof NumberSchema)) return new NumberSchema() - SchemaObject.call(this, { type: 'number' }) + MixedSchema.call(this, { type: 'number' }) this.withMutation(() => { this.transform(function(value) { if (this.isType(value)) return value - if (typeof value === 'boolean') return value ? 1 : 0 - return isDate(value) ? +value : parseFloat(value) + let parsed = parseFloat(value); + if (this.isType(parsed)) return parsed + + return NaN; }) }) } -inherits(NumberSchema, SchemaObject, { +inherits(NumberSchema, MixedSchema, { - _typeCheck(v) { - if (typeof v === 'number' && !(v !== +v)) return true - if (v instanceof Number && !isNaN(+v)) return true + _typeCheck(value) { + if (value instanceof Number) + value = value.valueOf(); - return false + return typeof value === 'number' && !isNaN(value) }, min(min, msg) { diff --git a/src/object.js b/src/object.js index c809308e3..d80cfc6ad 100644 --- a/src/object.js +++ b/src/object.js @@ -1,42 +1,46 @@ -'use strict'; -var MixedSchema = require('./mixed') - , Promise = require('universal-promise') - , toposort = require('toposort') - , locale = require('./locale.js').object - , split = require('property-expr').split - , Ref = require('./util/reference') - , c = require('case') - , sortByFields = require('./util/sortByFields') - , { - isObject - , transform - , inherits - , collectErrors - , isSchema, has } = require('./util/_'); - - -c.type('altCamel', function(str) { - let result = c.camel(str) - , idx = str.search(/[^_]/) +import changeCase from 'case'; +import has from 'lodash/has'; +import omit from 'lodash/omit'; +import mapKeys from 'lodash/mapKeys'; +import transform from 'lodash/transform'; - return idx === 0 ? result : (str.substr(0, idx) + result) -}) +import MixedSchema from './mixed'; +import { object as locale } from './locale.js'; +import sortFields from './util/sortFields'; +import sortByKeyOrder from './util/sortByKeyOrder'; +import inherits from './util/inherits'; +import runValidations, { propagateErrors } from './util/runValidations'; -let scopeError = value => err => { - err.value = value - throw err - } +let isObject = obj => Object.prototype.toString.call(obj) === '[object Object]'; +function unknown(ctx, value) { + var known = Object.keys(ctx.fields) + return Object.keys(value) + .filter(key => known.indexOf(key) === -1) +} + +/** + * maintain "private" fields + * `"__FOO_BAR"` becomes `"__fooBar"` not `"fooBar"` + */ +function camelize(str) { + let result = changeCase.camel(str) + , idx = str.search(/[^_]/) + + return idx === 0 ? result : (str.substr(0, idx) + result) +} module.exports = ObjectSchema function ObjectSchema(spec) { - if ( !(this instanceof ObjectSchema)) - return new ObjectSchema(spec) + if (!(this instanceof ObjectSchema)) + return new ObjectSchema(spec) MixedSchema.call(this, { type: 'object', default() { var dft = transform(this._nodes, (obj, key) => { - obj[key] = this.fields[key].default ? this.fields[key].default() : undefined + obj[key] = this.fields[key].default + ? this.fields[key].default() + : undefined }, {}) return Object.keys(dft).length === 0 ? undefined : dft @@ -60,8 +64,9 @@ function ObjectSchema(spec) { return null }) - if (spec) + if (spec) { this.shape(spec); + } }) } @@ -86,15 +91,18 @@ inherits(ObjectSchema, MixedSchema, { , extra = Object.keys(value).filter(v => this._nodes.indexOf(v) === -1) , props = this._nodes.concat(extra); - let innerOptions = { ...opts, parent: {}, __validating: false }; + let innerOptions = { + ...opts, + parent: {}, // is filled during the transform below + __validating: false, + }; - value = transform(props, function(obj, prop) { + value = transform(props, (obj, prop) => { let field = fields[prop] let exists = has(value, prop); if (field) { let fieldValue; - let strict = field._options && field._options.strict; if (field._strip === true) @@ -126,20 +134,17 @@ inherits(ObjectSchema, MixedSchema, { return MixedSchema.prototype._validate .call(this, _value, opts) - .catch(endEarly ? null : err => { - errors.push(err) - return err.value - }) + .catch(propagateErrors(endEarly, errors)) .then(value => { if (!recursive || !isObject(value)) { // only iterate though actual objects if (errors.length) throw errors[0] return value } - let validations = this._nodes.map((key) => { + let validations = this._nodes.map(key => { var path = (opts.path ? (opts.path + '.') : '') + key , field = this.fields[key] - , innerOptions = { ...opts, key, path, parent: value }; + , innerOptions = { ...opts, path, parent: value }; if (field) { // inner fields are always strict: @@ -154,14 +159,14 @@ inherits(ObjectSchema, MixedSchema, { return true }) - validations = endEarly - ? Promise.all(validations).catch(scopeError(value)) - : collectErrors({ validations, value, errors, - path: opts.path, - sort: sortByFields(this) - }) - - return validations.then(() => value) + return runValidations({ + validations, + value, + errors, + endEarly, + path: opts.path, + sort: sortByKeyOrder(this.fields) + }) }) }, @@ -182,9 +187,11 @@ inherits(ObjectSchema, MixedSchema, { next.fields = fields - if (excludes.length) - next._excludedEdges = next._excludedEdges.concat( - excludes.map(v => `${v[0]}-${v[1]}`)) // 'node-othernode' + if (excludes.length) { + let keys = excludes.map(([first, second]) => `${first}-${second}`); + + next._excludedEdges = next._excludedEdges.concat(keys) + } next._nodes = sortFields(fields, next._excludedEdges) @@ -192,17 +199,18 @@ inherits(ObjectSchema, MixedSchema, { }, from(from, to, alias) { - return this.transform( obj => { + return this.transform(obj => { var newObj = obj; if (obj == null) return obj if (has(obj, from)) { - newObj = transform(obj, (o, val, key) => key !== from && (o[key] = val), {}) + newObj = omit(obj, from); newObj[to] = obj[from] - if(alias) newObj[from] = obj[from] + if (alias) + newObj[from] = obj[from] } return newObj @@ -218,7 +226,11 @@ inherits(ObjectSchema, MixedSchema, { exclusive: true, message: message, test(value) { - return value == null || !noAllow || unknown(this.schema, value).length === 0 + return ( + value == null || + !noAllow || + unknown(this.schema, value).length === 0 + ) } }) @@ -228,59 +240,18 @@ inherits(ObjectSchema, MixedSchema, { return next }, - camelcase(){ - return this.transform(obj => obj == null ? obj - : transform(obj, (newobj, val, key ) => newobj[c.altCamel(key)] = val)) + camelcase() { + return this.transform(obj => + obj && mapKeys(obj, (_, key) => camelize(key)) + ) }, - constantcase(){ - return this.transform( obj => obj == null ? obj - : transform(obj, (newobj, val, key ) => newobj[c.constant(key)] = val)) - } + constantcase() { + return this.transform(obj => + obj && mapKeys(obj, (_, key) => changeCase.constant(key)) + ) + }, }) -function unknown(ctx, value) { - var known = Object.keys(ctx.fields) - return Object.keys(value) - .filter(key => known.indexOf(key) === -1) -} - -// ugly optimization avoiding a clone. clears default for recursive -// cast and resets it below; -// function tempClearDefault(schema, fn) { -// let hasDflt = has(schema, '_default') -// , dflt = schema._default; -// -// fn(schema) -// -// if (hasDflt) schema.default(dflt) -// else delete schema._default -// } - -function sortFields(fields, excludes = []){ - var edges = [], nodes = [] - - for (var key in fields) if (has(fields, key)) { - let value = fields[key]; - - if (!~nodes.indexOf(key)) - nodes.push(key) - - let addNode = depPath => { //eslint-disable-line no-loop-func - var node = split(depPath)[0] - - if (!~nodes.indexOf(node)) - nodes.push(node) - - if (!~excludes.indexOf(`${key}-${node}`)) - edges.push([key, node]) - } - - if (Ref.isRef(value) && !value.isContext) - addNode(value.path) - else if (isSchema(value) && value._deps) - value._deps.forEach(addNode) - } - - return toposort.array(nodes, edges).reverse() -} +ObjectSchema.prototype.camelCase = ObjectSchema.prototype.camelcase; +ObjectSchema.prototype.constantCase = ObjectSchema.prototype.constantcase; diff --git a/src/string.js b/src/string.js index 549384889..d4eb0a7f4 100644 --- a/src/string.js +++ b/src/string.js @@ -1,13 +1,12 @@ -'use strict'; -var MixedSchema = require('./mixed') - , { mixed, string: locale } = require('./locale.js') - , isAbsent = require('./util/isAbsent') - , inherits = require('./util/_').inherits; +import inherits from './util/inherits'; +import MixedSchema from './mixed'; +import { mixed, string as locale } from './locale'; +import isAbsent from './util/isAbsent'; -var rEmail = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; -var rUrl = /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i; +let rEmail = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; +let rUrl = /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i; -let hasLength = value => !isAbsent(value) && value.length > 0; +let hasLength = value => isAbsent(value) || value.length > 0; let isTrimmed = value => isAbsent(value) || value === value.trim() module.exports = StringSchema; @@ -21,8 +20,9 @@ function StringSchema(){ this.withMutation(() => { this.transform(function(value) { if (this.isType(value)) return value - return value == null ? '' - : value.toString ? value.toString() : '' + value + return value != null && value.toString + ? value.toString() + : value }) }) } @@ -30,11 +30,15 @@ function StringSchema(){ inherits(StringSchema, MixedSchema, { _typeCheck(value) { - return (typeof value === 'string') || (typeof value === 'object' && value instanceof String) + if (value instanceof String) + value = value.valueOf(); + + return typeof value === 'string' }, required(msg) { - var next = MixedSchema.prototype.required.call(this, msg || mixed.required ) + var next = MixedSchema.prototype + .required.call(this, msg || mixed.required) return next.test( 'required' diff --git a/src/util/_.js b/src/util/_.js deleted file mode 100644 index 5a9df9937..000000000 --- a/src/util/_.js +++ /dev/null @@ -1,127 +0,0 @@ -var Promise = require('universal-promise') - , ValidationError = require('./validation-error'); - -let toString = Object.prototype.toString - -let isObject = obj => obj && toString.call(obj) === '[object Object]'; - -let isPlainObject = obj => isObject(obj) && Object.getPrototypeOf(obj) === Object.prototype; - -let isDate = obj => Object.prototype.toString.call(obj) === '[object Date]' - -let isSchema = obj => obj && obj.__isYupSchema__ - -function settled(promises){ - let settle = promise => promise.then( - value => ({ fulfilled: true, value }), - value => ({ fulfilled: false, value })) - - return Promise.all(promises.map(settle)) -} - -function collectErrors({ validations, value, path, errors = [], sort }){ - // unwrap aggregate errors - errors = errors.inner && errors.inner.length - ? errors.inner : [].concat(errors) - - return settled(validations).then(results => { - let nestedErrors = results - .filter(r => !r.fulfilled) - .reduce((arr, r) => arr.concat(r.value), []) - - if (sort) nestedErrors.sort(sort) - //show parent errors after the nested ones: name.first, name - errors = nestedErrors.concat(errors) - - if (errors.length) - throw new ValidationError(errors, value, path) - }) -} - -function assign(target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i]; - - for (var key in source) if ( has(source, key)) - target[key] = source[key]; - } - - return target; -} - -function uniq(arr, iter){ - var seen = {} - - return arr.filter( (item, idx) => { - var key = iter(item, idx) - - if ( has(seen, key) ) return false - return seen[key] = true - }) -} - -function transform(obj, cb, seed){ - cb = cb.bind(null, seed = seed || (Array.isArray(obj) ? [] : {})) - - if( Array.isArray(obj)) - obj.forEach(cb) - else - for(var key in obj) if( has(obj, key) ) - cb(obj[key], key, obj) - - return seed -} - -function merge(target, source){ - for (var key in source) if ( has(source, key)) { - var targetVal = target[key] - , sourceVal = source[key]; - - if ( sourceVal === undefined ) - continue - - if ( isSchema(sourceVal) ) { - target[key] = isSchema(targetVal) - ? targetVal.concat(sourceVal) - : sourceVal - } - else if ( isObject(sourceVal) ) { - target[key] = isObject(targetVal) - ? merge(targetVal, sourceVal) - : sourceVal - } - else if ( Array.isArray(sourceVal) ) { - target[key] = Array.isArray(targetVal) - ? targetVal.concat(sourceVal) - : sourceVal - } - else - target[key] = source[key]; - } - - return target; -} - -function has(o, k){ - return o ? Object.prototype.hasOwnProperty.call(o, k) : false -} - -function inherits(ctor, superCtor, spec) { - ctor.prototype = Object.create(superCtor.prototype, { - constructor: { - value: ctor, - enumerable: false, - writable: true, - configurable: true - } - }); - - assign(ctor.prototype, spec) -} - -module.exports = { - inherits, uniq, has, - assign, merge, transform, - isSchema, isObject, isPlainObject, isDate, - settled, collectErrors -} diff --git a/src/util/clone.js b/src/util/clone.js index 1e7aadcbe..54438f686 100644 --- a/src/util/clone.js +++ b/src/util/clone.js @@ -2,18 +2,15 @@ // Copyright (c) 2011, Yahoo Inc. // All rights reserved. https://github.com/hapijs/hoek/blob/master/LICENSE -var isSchema = schema => schema && !!schema.__isYupSchema__; +import isSchema from './isSchema'; -module.exports = function clone(obj, seen) { +export default function clone(obj, seen) { var isFirst = !seen , isImmutable = isSchema(obj) && !isFirst if (typeof obj !== 'object' || obj === null || isImmutable) return obj; - // if (global.REPORT_CLONE && isFirst) - // throw new Error() //console.log('clone') - seen = seen || { orig: [], copy: [] }; var lookup = seen.orig.indexOf(obj); diff --git a/src/util/createValidation.js b/src/util/createValidation.js index 0585239c4..07440d3b4 100644 --- a/src/util/createValidation.js +++ b/src/util/createValidation.js @@ -1,16 +1,12 @@ -'use strict'; -var Promise = require('universal-promise') - , ValidationError = require('./validation-error') - , Ref = require('./reference') - , { transform } = require('./_'); +import Promise from 'universal-promise' +import mapValues from 'lodash/mapValues'; +import ValidationError from '../ValidationError' +import Ref from '../Reference' let formatError = ValidationError.formatError function resolveParams(oldParams, newParams, resolve) { - let start = { ...oldParams, ...newParams } - return transform(start, (obj, value, key) => { - obj[key] = resolve(value) - }) + return mapValues({ ...oldParams, ...newParams }, resolve) } function createErrorFactory({ value, label, resolve, ...opts}) { diff --git a/src/util/inherits.js b/src/util/inherits.js new file mode 100644 index 000000000..89a7c2474 --- /dev/null +++ b/src/util/inherits.js @@ -0,0 +1,13 @@ + +export default function inherits(ctor, superCtor, spec) { + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); + + Object.assign(ctor.prototype, spec); +} diff --git a/src/util/isSchema.js b/src/util/isSchema.js new file mode 100644 index 000000000..c214fd909 --- /dev/null +++ b/src/util/isSchema.js @@ -0,0 +1,3 @@ + + +export default obj => obj && obj.__isYupSchema__ diff --git a/src/util/isodate.js b/src/util/isodate.js index 32974b841..702fbf08d 100644 --- a/src/util/isodate.js +++ b/src/util/isodate.js @@ -22,12 +22,12 @@ module.exports = function parseIsoDate(date) { struct[3] = +struct[3] || 1; // allow arbitrary sub-second precision beyond milliseconds - struct[7] = struct[7] ? + (struct[7] + "00").substr(0, 3) : 0; + struct[7] = struct[7] ? + (struct[7] + '00').substr(0, 3) : 0; // timestamps without timezone identifiers should be considered local time - if ((struct[8] === undefined || struct[8] === '') && (struct[9] === undefined || struct[9] === '')) + if ((struct[8] === undefined || struct[8] === '') && (struct[9] === undefined || struct[9] === '')) timestamp = +new Date(struct[1], struct[2], struct[3], struct[4], struct[5], struct[6], struct[7]); - + else { if (struct[8] !== 'Z' && struct[9] !== undefined) { minutesOffset = struct[10] * 60 + struct[11]; @@ -38,9 +38,9 @@ module.exports = function parseIsoDate(date) { timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]); } - } - else + } + else timestamp = Date.parse ? Date.parse(date) : NaN; - + return timestamp; } diff --git a/src/util/merge.js b/src/util/merge.js new file mode 100644 index 000000000..80ddf6e99 --- /dev/null +++ b/src/util/merge.js @@ -0,0 +1,34 @@ +import has from 'lodash/has'; +import isSchema from './isSchema'; + +let isObject = obj => Object.prototype.toString.call(obj) === '[object Object]'; + +export default function merge(target, source){ + for (var key in source) if (has(source, key)) { + var targetVal = target[key] + , sourceVal = source[key]; + + if ( sourceVal === undefined ) + continue + + if (isSchema(sourceVal)) { + target[key] = isSchema(targetVal) + ? targetVal.concat(sourceVal) + : sourceVal + } + else if (isObject(sourceVal)) { + target[key] = isObject(targetVal) + ? merge(targetVal, sourceVal) + : sourceVal + } + else if (Array.isArray(sourceVal)) { + target[key] = Array.isArray(targetVal) + ? targetVal.concat(sourceVal) + : sourceVal + } + else + target[key] = source[key]; + } + + return target; +} diff --git a/src/util/reach.js b/src/util/reach.js index f20215b43..d56306f8c 100644 --- a/src/util/reach.js +++ b/src/util/reach.js @@ -1,5 +1,5 @@ -let { forEach } = require('property-expr') - , { has } = require('./_'); +import { forEach } from 'property-expr'; +import has from 'lodash/has'; let trim = part => part.substr(0, part.length - 1).substr(1) diff --git a/src/util/runValidations.js b/src/util/runValidations.js new file mode 100644 index 000000000..9af0d7a30 --- /dev/null +++ b/src/util/runValidations.js @@ -0,0 +1,69 @@ +import ValidationError from '../ValidationError'; + +let unwrapError = (errors = []) => + errors.inner && errors.inner.length + ? errors.inner + : [].concat(errors); + +function scopeToValue(promises, value) { + return Promise + .all(promises) + .catch(err => { + if (err.name === 'ValidationError') + err.value = value + throw err + }) + .then(() => value) +} + +/** + * If not failing on the first error, catch the errors + * and collect them in an array + */ +export function propagateErrors(endEarly, errors) { + return endEarly ? null : err => { + errors.push(err) + return err.value + } +} + +export function settled(promises){ + let settle = promise => promise.then( + value => ({ fulfilled: true, value }), + value => ({ fulfilled: false, value })) + + return Promise.all(promises.map(settle)) +} + + +export function collectErrors({ + validations, + value, + path, + errors = unwrapError(errors), + sort +}){ + return settled(validations).then(results => { + let nestedErrors = results + .filter(r => !r.fulfilled) + .reduce((arr, r) => arr.concat(r.value), []) + + if (sort) nestedErrors.sort(sort) + + //show parent errors after the nested ones: name.first, name + errors = nestedErrors.concat(errors) + + if (errors.length) + throw new ValidationError(errors, value, path) + + return value + }) +} + + +export default function runValidations({ endEarly, ...options }) { + if (endEarly) + return scopeToValue(options.validations, options.value) + + return collectErrors(options) +} diff --git a/src/util/scopeToValue.js b/src/util/scopeToValue.js new file mode 100644 index 000000000..4ccfb383f --- /dev/null +++ b/src/util/scopeToValue.js @@ -0,0 +1,16 @@ +import Promise from 'universal-promise'; + +/** + * Sets the error on a Validation error to a new + * value and re throws. + */ +export default function scopeToValue(promises, value) { + return Promise + .all(promises) + .catch(err => { + if (err.name === 'ValidationError') + err.value = value + throw err + }) + .then(() => value) +} diff --git a/src/util/set.js b/src/util/set.js index 6c14475fc..195e19031 100644 --- a/src/util/set.js +++ b/src/util/set.js @@ -1,4 +1,4 @@ -var { has } = require('./_') +import has from 'lodash/has'; module.exports = class BadSet { diff --git a/src/util/sortByFields.js b/src/util/sortByKeyOrder.js similarity index 74% rename from src/util/sortByFields.js rename to src/util/sortByKeyOrder.js index 04f4a8641..9483f3afe 100644 --- a/src/util/sortByFields.js +++ b/src/util/sortByKeyOrder.js @@ -1,5 +1,4 @@ - function findIndex(arr, err) { let idx = Infinity; arr.some((key, ii) => { @@ -12,8 +11,8 @@ function findIndex(arr, err) { return idx } -module.exports = function sortByFields(schema) { - let keys = Object.keys(schema.fields); +module.exports = function sortByKeyOrder(fields) { + let keys = Object.keys(fields); return (a, b) => { return findIndex(keys, a) - findIndex(keys, b) } diff --git a/src/util/sortFields.js b/src/util/sortFields.js new file mode 100644 index 000000000..5a429cfce --- /dev/null +++ b/src/util/sortFields.js @@ -0,0 +1,34 @@ +import toposort from 'toposort'; +import has from 'lodash/has'; +import { split } from 'property-expr'; +import Ref from '../Reference'; +import isSchema from './isSchema'; + +export default function sortFields(fields, excludes = []){ + var edges = [], nodes = [] + + function addNode(depPath, key) { + var node = split(depPath)[0] + + if (!~nodes.indexOf(node)) + nodes.push(node) + + if (!~excludes.indexOf(`${key}-${node}`)) + edges.push([key, node]) + } + + for (var key in fields) if (has(fields, key)) { + let value = fields[key]; + + if (!~nodes.indexOf(key)) + nodes.push(key) + + if (Ref.isRef(value) && !value.isContext) + addNode(value.path, key) + + else if (isSchema(value) && value._deps) + value._deps.forEach(path => addNode(path, key)) + } + + return toposort.array(nodes, edges).reverse() +} diff --git a/test/array.js b/test/array.js index 29767a8b0..834dbce9e 100644 --- a/test/array.js +++ b/test/array.js @@ -1,15 +1,8 @@ -'use strict'; -/* global describe, it */ -var chai = require('chai') - , chaiAsPromised = require('chai-as-promised') - , Promise = require('promise/src/es6-extensions') - , string = require('../src/string') - , number = require('../src/number') - , object = require('../src/object') - , array = require('../src/array'); - -chai.use(chaiAsPromised); -chai.should(); +import Promise from 'promise/src/es6-extensions' +import string from '../src/string' +import number from '../src/number' +import object from '../src/object' +import array from '../src/array' describe('Array types', function(){ @@ -22,16 +15,16 @@ describe('Array types', function(){ it ('should return null for failed casts', () => { expect( - array().cast('asfasf')).to.equal(null) + array().cast('asfasf', { assert: false })).to.equal(null) expect( - array().cast(null)).to.equal(null) + array().cast(null, { assert: false })).to.equal(null) }) it ('should recursively cast fields', () => { array().of(number()) - .cast(['4', 5, false]) - .should.eql([4, 5, 0]) + .cast(['4', '5']) + .should.eql([4, 5]) array().of(string()) .cast(['4', 5, false]) @@ -126,14 +119,14 @@ describe('Array types', function(){ .test('name', 'oops', function(){ return false }) return Promise.all([ - inst.validate([{ str: null }]).should.be.rejected + inst.validate([{ str: '' }]).should.be.rejected .then(function(err){ err.value.should.eql([{ str: '' }]) err.errors.length.should.equal(1) err.errors.should.eql(['oops']) }), - inst.validate([{ str: null }], { abortEarly: false }).should.be.rejected + inst.validate([{ str: '' }], { abortEarly: false }).should.be.rejected .then(function(err) { err.value.should.eql([{ str: '' }]) diff --git a/test/bool.js b/test/bool.js index addc01c82..5490cda99 100644 --- a/test/bool.js +++ b/test/bool.js @@ -15,14 +15,17 @@ describe('Boolean types', function(){ var inst = bool(); inst.cast('true').should.equal(true) + inst.cast('True').should.equal(true) + inst.cast('false').should.equal(false) + inst.cast('False').should.equal(false) inst.cast(1).should.equal(true) inst.cast(0).should.equal(false) - chai.expect( - inst.cast(null)).to.equal(false) + TestHelpers + .castAndShouldFail(inst, 'foo') - chai.expect( - inst.nullable().cast(null)).to.equal(null) + TestHelpers + .castAndShouldFail(inst, 'bar1') }) it('should handle DEFAULT', function(){ @@ -39,6 +42,8 @@ describe('Boolean types', function(){ inst.isType(false).should.equal(true) inst.isType('true').should.equal(false) inst.isType(NaN).should.equal(false) + inst.isType(new Number('foooo')).should.equal(false) + inst.isType(34545).should.equal(false) inst.isType(new Boolean(false)).should.equal(true) chai.expect( @@ -51,7 +56,7 @@ describe('Boolean types', function(){ var inst = bool().required() return Promise.all([ - bool().isValid(null).should.eventually.equal(true), //coerced to false + bool().isValid('1').should.eventually.equal(true), bool().strict().isValid(null).should.eventually.equal(false), diff --git a/test/date.js b/test/date.js index bcaf22d59..e61d3b0be 100644 --- a/test/date.js +++ b/test/date.js @@ -1,11 +1,5 @@ -'use strict'; -var chai = require('chai') - , chaiAsPromised = require('chai-as-promised') - , Promise = require('promise/src/es6-extensions') - , { ref, date } = require('../src'); - -chai.use(chaiAsPromised); -chai.should(); +import Promise from 'promise/src/es6-extensions'; +import { ref, date } from '../src'; function isValidDate(date){ return date instanceof Date && !isNaN(date.getTime()) @@ -13,19 +7,22 @@ function isValidDate(date){ describe('Date types', function(){ - it('should CAST correctly', function(){ + it('should CAST correctly', function(){ var inst = date() - inst.cast(null).should.not.satisfy(isValidDate) - inst.cast('').should.not.satisfy(isValidDate) - - inst.cast(new Date()).should.be.a('date') inst.cast(new Date()).should.be.a('date') inst.cast('jan 15 2014').should.eql(new Date(2014, 0, 15)) inst.cast('2014-09-23T19:25:25Z').should.eql(new Date(1411500325000)) }) + it('should return invalid date for failed casts', function(){ + var inst = date() + + inst.cast(null, { assert: false }).should.not.satisfy(isValidDate) + inst.cast('', { assert: false }).should.not.satisfy(isValidDate) + }) + it('should type check', function(){ var inst = date() diff --git a/test/helpers.js b/test/helpers.js new file mode 100644 index 000000000..d2c98d14e --- /dev/null +++ b/test/helpers.js @@ -0,0 +1,46 @@ +import typeOf from 'type-name'; + +export let castAndShouldFail = (schema, value) => { + (()=> schema.cast(value)) + .should.throw( + TypeError, + new RegExp(`Got "${typeOf(value)}" instead`, 'gi') + ) +} + +export let castAll = (inst, { invalid = [], valid = [] }) => { + valid.forEach(([value, result, schema = inst ]) => { + it(`should cast ${JSON.stringify(value)} to ${JSON.stringify(result)}`, () => + expect( + schema.cast(value) + ) + .to.equal(result) + ) + }) + + invalid.forEach((value) => { + it(`should not cast ${JSON.stringify(value)}`, () => + castAndShouldFail(inst, value) + ) + }) +} + +export let validateAll = (inst, { valid = [], invalid = [] }) => { + runValidations(valid, true) + runValidations(invalid, false) + + function runValidations(arr, isValid) { + arr.forEach((config) => { + let value = config, schema = inst; + + if (Array.isArray(config)) + [ value, schema ] = config; + + it(`${JSON.stringify(value)} should be ${isValid ? 'valid' : 'invalid'}`, + () => schema.isValid(value).should.become(isValid) + ) + }) + } + + +} diff --git a/test/mixed.js b/test/mixed.js index 4cf01c550..63c9901e8 100644 --- a/test/mixed.js +++ b/test/mixed.js @@ -1,20 +1,14 @@ -'use strict'; -/*global describe, it */ -var chai = require('chai') - , chaiAsPromised = require('chai-as-promised') - , ValidationError = require('../src/util/validation-error') - , Promise = require('promise/src/es6-extensions') - , mixed = require('../src/mixed') - , object = require('../src/object') - , string = require('../src/string') - , reach = require('../src/util/reach'); - -chai.use(chaiAsPromised); -chai.should(); + +import Promise from 'promise/src/es6-extensions'; +import mixed from '../src/mixed'; +import object from '../src/object'; +import string from '../src/string'; +import ValidationError from '../src/ValidationError'; +import reach from '../src/util/reach'; let noop = function(){} -describe( 'Mixed Types ', function(){ +describe.only( 'Mixed Types ', function(){ it('should be immutable', function(){ var inst = mixed(), next; @@ -51,7 +45,7 @@ describe( 'Mixed Types ', function(){ error = await inst.validate(5, { abortEarly: false }).should.be.rejected - chai.expect(error.type).to.not.exist + expect(error.type).to.not.exist error.message.should.equal('must be a string!') error.inner.length.should.equal(1) }) @@ -250,14 +244,14 @@ describe( 'Mixed Types ', function(){ name: 'max', test() { this.path.should.equal('test') - this.parent.should.eql({ other: 5, test : 'hi' }) + this.parent.should.eql({ other: 5, test: 'hi' }) this.options.context.should.eql({ user: 'jason' }) done() } }) }) - inst.validate({ other: 5, test : 'hi' }, { context: { user: 'jason' } }) + inst.validate({ other: 5, test: 'hi' }, { context: { user: 'jason' } }) }) it('tests can return an error', function(){ @@ -320,11 +314,11 @@ describe( 'Mixed Types ', function(){ inst.isValid(5, function(err, valid){ valid.should.equal(true) - chai.expect(err).to.equal(null) + expect(err).to.equal(null) inst.strict().validate(5, function(err, value){ err.should.be.an.instanceOf(ValidationError) - chai.expect(value).to.equal(undefined) + expect(value).to.equal(undefined) done() }) }) @@ -349,7 +343,7 @@ describe( 'Mixed Types ', function(){ })) }) - it ('should have teh correct number of tests', () => { + it ('should have the correct number of tests', () => { reach(next, 'str').tests.length.should.equal(3) // presence, alt presence, and trim }) @@ -405,7 +399,7 @@ describe( 'Mixed Types ', function(){ it('concat should maintain undefined defaults', function(){ var inst = string().default('hi') - chai.expect( + expect( inst.concat(string().default(undefined)).default()).to.equal(undefined) }) diff --git a/test/number.js b/test/number.js index de5b47748..8edee6b33 100644 --- a/test/number.js +++ b/test/number.js @@ -1,40 +1,68 @@ -'use strict'; -/* global describe, it */ -var chai = require('chai') - , chaiAsPromised = require('chai-as-promised') - , Promise = require('promise/src/es6-extensions') - , number = require('../src/number'); +import Promise from 'promise/src/es6-extensions'; +import number from '../src/number'; -chai.use(chaiAsPromised); -chai.should(); +describe('Number types', function() { -describe('Number types', function(){ - - it('should CAST correctly', function(){ - - var inst = number(), date = new Date() - - chai.expect( - inst.cast(null)).to.eql(NaN) - - inst.cast('5').should.equal(5) - inst.cast('').should.eql(NaN) - inst.cast(3).should.equal(3) - inst.cast(false).should.equal(0) - inst.cast(true).should.equal(1) - inst.cast(date).should.equal(date.getTime()) + it('is newable', () => { + let schema = new number(); + schema.integer().required() + }) - inst.integer().cast(45.55).should.equal(45) - inst.round('Floor').cast(45.99999).should.equal(45) - inst.round('ceIl').cast(45.1111).should.equal(46) - inst.round().cast(45.444444).should.equal(45) + it('is extensible', () => { + class MyNumber extends number { + foo() { + return this; + } + } - ;(function(){ inst.round('fasf') }).should.throw(TypeError) + new MyNumber().foo().integer().required() + }) - chai.expect(inst.nullable() - .integer() - .round() - .cast(null)).to.equal(null) + describe('casting', ()=> { + let schema = number(); + + TestHelpers.castAll(schema, { + valid: [ + ['5', 5], + [3, 3], + //[new Number(5), 5], + [' 5.656 ', 5.656], + ], + invalid: [ + '', + false, + true, + new Date(), + new Number('foo') + ] + }) + + it('should round', () => { + schema.round('floor').cast(45.99999).should.equal(45) + schema.round('ceIl').cast(45.1111).should.equal(46) + schema.round().cast(45.444444).should.equal(45) + + expect( + schema.nullable() + .integer() + .round() + .cast(null) + ).to.equal(null) + + ;(function(){ schema.round('fasf') }).should.throw(TypeError) + }) + + it('should truncate', () => { + schema.truncate().cast(45.55).should.equal(45) + }) + + it ('should return NaN for failed casts', () => { + expect( + number().cast('asfasf', { assert: false })).to.eql(NaN) + + expect( + number().cast(null, { assert: false })).to.eql(NaN) + }) }) it('should handle DEFAULT', function(){ @@ -49,6 +77,7 @@ describe('Number types', function(){ inst.isType(5).should.equal(true) inst.isType(new Number(5)).should.equal(true) + inst.isType(new Number('foo')).should.equal(false) inst.isType(false).should.equal(false) inst.isType(null).should.equal(false) inst.isType(NaN).should.equal(false) @@ -73,40 +102,69 @@ describe('Number types', function(){ ]) }) - it('should check MIN correctly', function(){ - var v = number().min(5); - - return Promise.all([ - v.isValid(7).should.eventually.equal(true), - v.isValid(2).should.eventually.equal(false), - v.isValid(35738787838).should.eventually.equal(true), - - v.min(10).min(15).isValid(14).should.eventually.equal(false), - - v.isValid(new Date).should.eventually.equal(true), - - v.isValid(null).should.eventually.equal(false), // -> NaN fails type check + describe('min', () => { + var schema = number().min(5); + + TestHelpers.validateAll(schema, { + valid: [ + 7, + 35738787838, + [null, schema.nullable()] + ], + invalid: [ + 2, + null, + [14, schema.min(10).min(15)] + ] + }) + }) - v.nullable().isValid(null).should.eventually.equal(true), - ]) + describe('max', () => { + var schema = number().max(5); + + TestHelpers.validateAll(schema, { + valid: [ + 4, + -5222, + [null, schema.nullable()] + ], + invalid: [ + 10, + null, + [16, schema.max(20).max(15)] + ] + }) }) - it('should check MAX correctly', function(){ - var v = number().max(5); + describe('integer', ()=> { + TestHelpers.validateAll( + number().integer(), + { + valid: [ + 4, + -5222, + ], + invalid: [ + 10.53, + 0.1 * 0.2, + -34512535.626, + 3.12312e+51, + new Date(), + ] + } + ) + }) + it('should check integer', function(){ + var v = number().positive(); return Promise.all([ - v.isValid(4).should.eventually.equal(true), - v.isValid(10).should.eventually.equal(false), - v.isValid(-5222).should.eventually.equal(true), - - v.isValid(false).should.eventually.equal(true), - v.isValid(new Date).should.eventually.equal(false), - - v.max(10).max(15).isValid(16).should.eventually.equal(false), + v.isValid(7).should.eventually.equal(true), - v.isValid(null).should.eventually.equal(false), // null -> NaN fails type check + v.isValid(0).should.eventually.equal(true), - v.nullable().isValid(null).should.eventually.equal(true), + v.validate(-4).should.be.rejected.then(null, function(err){ + err.errors[0].should.contain('this must be a positive number') + }) ]) }) diff --git a/test/object.js b/test/object.js index 9dd8ae8ed..f54fa8997 100644 --- a/test/object.js +++ b/test/object.js @@ -1,16 +1,9 @@ -'use strict'; -/*global describe, it */ -var chai = require('chai') - , chaiAsPromised = require('chai-as-promised') - , Promise = require('promise/src/es6-extensions') - , { - mixed, string, date, number - , bool, array, object, ref, lazy, reach - } = require('../src'); - -chai.use(chaiAsPromised); -chai.should(); +import Promise from 'promise/src/es6-extensions'; +import { + mixed, string, date, number + , bool, array, object, ref, lazy, reach +} from '../src'; describe('Object types', function(){ @@ -24,15 +17,15 @@ describe('Object types', function(){ }) it ('should return null for failed casts', () => { - chai.expect( - object().cast('dfhdfh')).to.equal(null) + expect( + object().cast('dfhdfh', { assert: false })).to.equal(null) }) it ('should recursively cast fields', () => { var obj = { num: '5', str: 'hello', - arr: ['4', 5, false], + arr: ['4', 5], dte: '2014-09-23T19:25:25Z', nested: { str: 5 }, arrNested: [{ num: 5 }, { num: '5' }] @@ -51,7 +44,7 @@ describe('Object types', function(){ .cast(obj).should.eql({ num: 5, str: 'hello', - arr: [4, 5, 0], + arr: [4, 5], dte: new Date(1411500325000), nested: { str: '5' }, arrNested: [{ num: 5 }, { num: 5 }] @@ -74,14 +67,14 @@ describe('Object types', function(){ obj = { num: '4', str: 'hello', - arr: ['4', 5, false], + arr: ['4', 5, 6], dte: '2014-09-23T19:25:25Z', nested: { str: 5 }, arrNested: [{ num: 5 }, { num: '2' }] } }) - it ('should run validations recursively', async () => { + it('should run validations recursively', async () => { let error = await inst.validate(obj).should.be.rejected; error.errors.length.should.equal(1) @@ -468,7 +461,7 @@ describe('Object types', function(){ return Promise.all([ inst - .validate({ nest: { str: null } }).should.be.rejected + .validate({ nest: { str: '' } }).should.be.rejected .then(function(err) { err.value.should.eql({ nest: { str: '' } }) err.errors.length.should.equal(1) @@ -478,7 +471,11 @@ describe('Object types', function(){ }), inst - .validate({ nest: { str: null } }, { abortEarly: false }).should.be.rejected + .validate( + { nest: { str: '' } }, + { abortEarly: false } + ) + .should.be.rejected .then(function(err) { err.value.should.eql({ nest: { str: '' } }) err.errors.length.should.equal(2) @@ -496,7 +493,7 @@ describe('Object types', function(){ }) let err = await inst.validate( - { foo: 'foo', bar: null }, + { foo: 'foo' }, { abortEarly: false }).should.rejected; err.errors.should.eql([ @@ -608,9 +605,9 @@ describe('Object types', function(){ }) .default(undefined) - chai.expect(inst.concat(object()).default()).to.equal(undefined) + expect(inst.concat(object()).default()).to.equal(undefined) - chai.expect(inst.concat(object().default({})).default()).to.eql({}) + expect(inst.concat(object().default({})).default()).to.eql({}) }) it('should handle nested conditionals', function(){ @@ -659,14 +656,14 @@ describe('Object types', function(){ inst.cast({ CON_STAT: 5, CaseStatus: 6, 'hi john': 4 }) .should.eql({ conStat: 5, caseStatus: 6, hiJohn: 4 }) - chai.expect(inst + expect(inst .nullable() .cast(null)).to.equal(null) }) it('should camelCase with leading underscore', function(){ var inst = object().camelcase() - + inst .cast({ CON_STAT: 5, __isNew: true, __IS_FUN: true }) .should @@ -684,7 +681,7 @@ describe('Object types', function(){ inst.cast({ conStat: 5, CaseStatus: 6, 'hi john': 4 }) .should.eql({ CON_STAT: 5, CASE_STATUS: 6, HI_JOHN: 4 }) - chai.expect(inst + expect(inst .nullable() .cast(null)).to.equal(null) }) diff --git a/test/string.js b/test/string.js index 0146afae3..91407ac07 100644 --- a/test/string.js +++ b/test/string.js @@ -1,45 +1,69 @@ 'use strict'; -/* global describe, it */ -var chai = require('chai') - , Promise = require('promise/src/es6-extensions') - , sinonChai = require('sinon-chai') - , chaiAsPromised = require('chai-as-promised') - , { string, number, object, ref } = require('../src'); - -chai.use(chaiAsPromised); -chai.use(sinonChai); -chai.should(); +import Promise from 'promise/src/es6-extensions' +import { string, number, object, ref } from '../src'; describe('String types', function(){ - it('should CAST correctly', function(){ - - var inst = string() - - inst.cast(5).should.equal('5') - - chai.expect( - inst.cast(null)).to.equal('') - - chai.expect( - inst.nullable().cast(null)).to.equal(null) - - inst.cast('3').should.equal('3') - inst.cast(false).should.equal('false') - inst.cast(true).should.equal('true') + describe('casting', ()=> { + let schema = string(); + + TestHelpers.castAll(schema, { + valid: [ + [5, '5'], + ['3', '3'], + //[new String('foo'), 'foo'], + ['', ''], + [true, 'true'], + [false, 'false'], + [0, '0'], + [null, null, schema.nullable()] + ], + invalid: [ + undefined, + null, + ] + }) - chai.expect(inst.cast()).to.equal(undefined) + describe('ensure', () => { + let schema = string().ensure(); + + TestHelpers.castAll( + schema, + { + valid: [ + [5, '5'], + ['3', '3'], + [null, ''], + [undefined, ''], + [null, '', schema.default('foo')], + [undefined, 'foo', schema.default('foo')], + ] + } + ) + }) - inst.trim().cast(' 3 ').should.equal('3') + it('should trim', () => { + schema.trim().cast(' 3 ').should.equal('3') + }) - inst.lowercase().cast('HellO JohN').should.equal('hello john') - inst.uppercase().cast('HellO JohN').should.equal('HELLO JOHN') + it('should transform to lowercase', () => { + schema.lowercase() + .cast('HellO JohN') + .should.equal('hello john') + }) + it('should transform to lowercase', () => { + schema.uppercase() + .cast('HellO JohN') + .should.equal('HELLO JOHN') + }) - chai.expect(inst.nullable() - .trim() - .lowercase() - .uppercase() - .cast(null)).to.equal(null) + it('should handle nulls', () => { + expect(schema.nullable() + .trim() + .lowercase() + .uppercase() + .cast(null)).to.equal(null) + }) }) it('should handle DEFAULT', function(){ @@ -118,10 +142,7 @@ describe('String types', function(){ v.isValid('bigdfdsfsdf').should.eventually.equal(false), v.isValid('no').should.eventually.equal(true), - v.isValid(5).should.eventually.equal(true), - v.isValid(new Date()).should.eventually.equal(false), - - v.isValid(null).should.eventually.equal(true), + v.isValid(null).should.eventually.equal(false), v.nullable().isValid(null).should.eventually.equal(true), diff --git a/test/yup.js b/test/yup.js index a6e9b14ff..f4e1d8381 100644 --- a/test/yup.js +++ b/test/yup.js @@ -1,16 +1,10 @@ -'use strict'; -/*global describe, it */ -var Promise = require('promise/src/es6-extensions') - , chai = require('chai') - , chaiAsPromised = require('chai-as-promised') - , reach = require('../src/util/reach') - , BadSet = require('../src/util/set') - , { object, array, string, lazy, number } = require('../src') - , _ = require('../src/util/_'); +import Promise from 'promise/src/es6-extensions'; +import reach from '../src/util/reach'; +import BadSet from '../src/util/set'; +import merge from '../src/util/merge'; +import { settled } from '../src/util/runValidations'; -chai.use(chaiAsPromised); - -chai.should(); +import { object, array, string, lazy, number } from '../src'; describe('Yup', function(){ @@ -18,18 +12,10 @@ describe('Yup', function(){ require('../lib') }) - it('should uniq', function(){ - _.uniq([1, 1, 2, 3, 4, 3], function(i){ return i}) - .should.eql([1, 2, 3, 4]) - - _.uniq([{ a: 1}, { a: 2}, { a: 3}, { a: 1}], function(i){ return i.a}) - .should.deep.eql([{ a: 1}, { a: 2}, { a: 3}]) - }) - it('should do settled', function(){ return Promise.all([ - _.settled([Promise.resolve('hi'), Promise.reject('error')]).should.be.fulfilled + settled([Promise.resolve('hi'), Promise.reject('error')]).should.be.fulfilled .then(function (results) { results.length.should.equal(2) results[0].fulfilled.should.equal(true) @@ -42,10 +28,9 @@ describe('Yup', function(){ it('should merge', function(){ var a = { a: 1, b: 'hello', c: [1, 2, 3], d: { a: /hi/ }, e: { b: 5} } - var b = { a: 4, c: [4, 5, 3], d: { b: 'hello' }, f: { c: 5}, g: null } - _.merge(a, b).should.deep.eql({ + merge(a, b).should.deep.eql({ a: 4, b: 'hello', c: [1, 2, 3, 4, 5, 3], diff --git a/tests-webpack.js b/tests-webpack.js index 4272c1028..1347e3845 100644 --- a/tests-webpack.js +++ b/tests-webpack.js @@ -7,7 +7,9 @@ chai.use(require('chai-as-promised')) chai.use(require('sinon-chai')) chai.should(); + global.expect = window.expect = chai.expect; +global.TestHelpers = window.TestHelpers = require('./test/helpers'); var testsContext = require.context('./test', true);