UNPKG

yup

Version:

Dead simple Object schema validation

1,589 lines (1,540 loc) 76.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var propertyExpr = require('property-expr'); var tinyCase = require('tiny-case'); var toposort = require('toposort'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var toposort__default = /*#__PURE__*/_interopDefaultLegacy(toposort); const toString = Object.prototype.toString; const errorToString = Error.prototype.toString; const regExpToString = RegExp.prototype.toString; const symbolToString = typeof Symbol !== 'undefined' ? Symbol.prototype.toString : () => ''; const SYMBOL_REGEXP = /^Symbol\((.*)\)(.*)$/; function printNumber(val) { if (val != +val) return 'NaN'; const isNegativeZero = val === 0 && 1 / val < 0; return isNegativeZero ? '-0' : '' + val; } function printSimpleValue(val, quoteStrings = false) { if (val == null || val === true || val === false) return '' + val; const typeOf = typeof val; if (typeOf === 'number') return printNumber(val); if (typeOf === 'string') return quoteStrings ? `"${val}"` : val; if (typeOf === 'function') return '[Function ' + (val.name || 'anonymous') + ']'; if (typeOf === 'symbol') return symbolToString.call(val).replace(SYMBOL_REGEXP, 'Symbol($1)'); const tag = toString.call(val).slice(8, -1); if (tag === 'Date') return isNaN(val.getTime()) ? '' + val : val.toISOString(val); if (tag === 'Error' || val instanceof Error) return '[' + errorToString.call(val) + ']'; if (tag === 'RegExp') return regExpToString.call(val); return null; } function printValue(value, quoteStrings) { let result = printSimpleValue(value, quoteStrings); if (result !== null) return result; return JSON.stringify(value, function (key, value) { let result = printSimpleValue(this[key], quoteStrings); if (result !== null) return result; return value; }, 2); } function toArray(value) { return value == null ? [] : [].concat(value); } let _Symbol$toStringTag, _Symbol$hasInstance, _Symbol$toStringTag2; let strReg = /\$\{\s*(\w+)\s*\}/g; _Symbol$toStringTag = Symbol.toStringTag; class ValidationErrorNoStack { constructor(errorOrErrors, value, field, type) { this.name = void 0; this.message = void 0; this.value = void 0; this.path = void 0; this.type = void 0; this.params = void 0; this.errors = void 0; this.inner = void 0; this[_Symbol$toStringTag] = 'Error'; this.name = 'ValidationError'; this.value = value; this.path = field; this.type = type; this.errors = []; this.inner = []; toArray(errorOrErrors).forEach(err => { if (ValidationError.isError(err)) { this.errors.push(...err.errors); const innerErrors = err.inner.length ? err.inner : [err]; this.inner.push(...innerErrors); } else { this.errors.push(err); } }); this.message = this.errors.length > 1 ? `${this.errors.length} errors occurred` : this.errors[0]; } } _Symbol$hasInstance = Symbol.hasInstance; _Symbol$toStringTag2 = Symbol.toStringTag; class ValidationError extends Error { static formatError(message, params) { // Attempt to make the path more friendly for error message interpolation. const path = params.label || params.path || 'this'; // Store the original path under `originalPath` so it isn't lost to custom // message functions; e.g., ones provided in `setLocale()` calls. params = Object.assign({}, params, { path, originalPath: params.path }); if (typeof message === 'string') return message.replace(strReg, (_, key) => printValue(params[key])); if (typeof message === 'function') return message(params); return message; } static isError(err) { return err && err.name === 'ValidationError'; } constructor(errorOrErrors, value, field, type, disableStack) { const errorNoStack = new ValidationErrorNoStack(errorOrErrors, value, field, type); if (disableStack) { return errorNoStack; } super(); this.value = void 0; this.path = void 0; this.type = void 0; this.params = void 0; this.errors = []; this.inner = []; this[_Symbol$toStringTag2] = 'Error'; this.name = errorNoStack.name; this.message = errorNoStack.message; this.type = errorNoStack.type; this.value = errorNoStack.value; this.path = errorNoStack.path; this.errors = errorNoStack.errors; this.inner = errorNoStack.inner; if (Error.captureStackTrace) { Error.captureStackTrace(this, ValidationError); } } static [_Symbol$hasInstance](inst) { return ValidationErrorNoStack[Symbol.hasInstance](inst) || super[Symbol.hasInstance](inst); } } let mixed = { default: '${path} is invalid', required: '${path} is a required field', defined: '${path} must be defined', notNull: '${path} cannot be null', oneOf: '${path} must be one of the following values: ${values}', notOneOf: '${path} must not be one of the following values: ${values}', notType: ({ path, type, value, originalValue }) => { const castMsg = originalValue != null && originalValue !== value ? ` (cast from the value \`${printValue(originalValue, true)}\`).` : '.'; return type !== 'mixed' ? `${path} must be a \`${type}\` type, ` + `but the final value was: \`${printValue(value, true)}\`` + castMsg : `${path} must match the configured type. ` + `The validated value was: \`${printValue(value, true)}\`` + castMsg; } }; let string = { length: '${path} must be exactly ${length} characters', 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', uuid: '${path} must be a valid UUID', datetime: '${path} must be a valid ISO date-time', datetime_precision: '${path} must be a valid ISO date-time with a sub-second precision of exactly ${precision} digits', datetime_offset: '${path} must be a valid ISO date-time with UTC "Z" timezone', trim: '${path} must be a trimmed string', lowercase: '${path} must be a lowercase string', uppercase: '${path} must be a upper case string' }; let number = { min: '${path} must be greater than or equal to ${min}', max: '${path} must be less than or equal to ${max}', lessThan: '${path} must be less than ${less}', moreThan: '${path} must be greater than ${more}', positive: '${path} must be a positive number', negative: '${path} must be a negative number', integer: '${path} must be an integer' }; let date = { min: '${path} field must be later than ${min}', max: '${path} field must be at earlier than ${max}' }; let boolean = { isValue: '${path} field must be ${value}' }; let object = { noUnknown: '${path} field has unspecified keys: ${unknown}', exact: '${path} object contains unknown properties: ${properties}' }; let array = { min: '${path} field must have at least ${min} items', max: '${path} field must have less than or equal to ${max} items', length: '${path} must have ${length} items' }; let tuple = { notType: params => { const { path, value, spec } = params; const typeLen = spec.types.length; if (Array.isArray(value)) { if (value.length < typeLen) return `${path} tuple value has too few items, expected a length of ${typeLen} but got ${value.length} for value: \`${printValue(value, true)}\``; if (value.length > typeLen) return `${path} tuple value has too many items, expected a length of ${typeLen} but got ${value.length} for value: \`${printValue(value, true)}\``; } return ValidationError.formatError(mixed.notType, params); } }; var locale = Object.assign(Object.create(null), { mixed, string, number, date, object, array, boolean, tuple }); const isSchema = obj => obj && obj.__isYupSchema__; class Condition { static fromOptions(refs, config) { if (!config.then && !config.otherwise) throw new TypeError('either `then:` or `otherwise:` is required for `when()` conditions'); let { is, then, otherwise } = config; let check = typeof is === 'function' ? is : (...values) => values.every(value => value === is); return new Condition(refs, (values, schema) => { var _branch; let branch = check(...values) ? then : otherwise; return (_branch = branch == null ? void 0 : branch(schema)) != null ? _branch : schema; }); } constructor(refs, builder) { this.fn = void 0; this.refs = refs; this.refs = refs; this.fn = builder; } resolve(base, options) { let values = this.refs.map(ref => // TODO: ? operator here? ref.getValue(options == null ? void 0 : options.value, options == null ? void 0 : options.parent, options == null ? void 0 : options.context)); let schema = this.fn(values, base, options); if (schema === undefined || // @ts-ignore this can be base schema === base) { return base; } if (!isSchema(schema)) throw new TypeError('conditions must return a schema object'); return schema.resolve(options); } } const prefixes = { context: '$', value: '.' }; function create$9(key, options) { return new Reference(key, options); } class Reference { constructor(key, options = {}) { this.key = void 0; this.isContext = void 0; this.isValue = void 0; this.isSibling = void 0; this.path = void 0; this.getter = void 0; this.map = void 0; if (typeof key !== 'string') throw new TypeError('ref must be a string, got: ' + key); this.key = key.trim(); if (key === '') throw new TypeError('ref must be a non-empty string'); this.isContext = this.key[0] === prefixes.context; this.isValue = this.key[0] === prefixes.value; this.isSibling = !this.isContext && !this.isValue; let prefix = this.isContext ? prefixes.context : this.isValue ? prefixes.value : ''; this.path = this.key.slice(prefix.length); this.getter = this.path && propertyExpr.getter(this.path, true); this.map = options.map; } getValue(value, parent, context) { let result = this.isContext ? context : this.isValue ? value : parent; if (this.getter) result = this.getter(result || {}); if (this.map) result = this.map(result); return result; } /** * * @param {*} value * @param {Object} options * @param {Object=} options.context * @param {Object=} options.parent */ cast(value, options) { return this.getValue(value, options == null ? void 0 : options.parent, options == null ? void 0 : options.context); } resolve() { return this; } describe() { return { type: 'ref', key: this.key }; } toString() { return `Ref(${this.key})`; } static isRef(value) { return value && value.__isYupRef; } } // @ts-ignore Reference.prototype.__isYupRef = true; const isAbsent = value => value == null; function createValidation(config) { function validate({ value, path = '', options, originalValue, schema }, panic, next) { const { name, test, params, message, skipAbsent } = config; let { parent, context, abortEarly = schema.spec.abortEarly, disableStackTrace = schema.spec.disableStackTrace } = options; function resolve(item) { return Reference.isRef(item) ? item.getValue(value, parent, context) : item; } function createError(overrides = {}) { const nextParams = Object.assign({ value, originalValue, label: schema.spec.label, path: overrides.path || path, spec: schema.spec, disableStackTrace: overrides.disableStackTrace || disableStackTrace }, params, overrides.params); for (const key of Object.keys(nextParams)) nextParams[key] = resolve(nextParams[key]); const error = new ValidationError(ValidationError.formatError(overrides.message || message, nextParams), value, nextParams.path, overrides.type || name, nextParams.disableStackTrace); error.params = nextParams; return error; } const invalid = abortEarly ? panic : next; let ctx = { path, parent, type: name, from: options.from, createError, resolve, options, originalValue, schema }; const handleResult = validOrError => { if (ValidationError.isError(validOrError)) invalid(validOrError);else if (!validOrError) invalid(createError());else next(null); }; const handleError = err => { if (ValidationError.isError(err)) invalid(err);else panic(err); }; const shouldSkip = skipAbsent && isAbsent(value); if (shouldSkip) { return handleResult(true); } let result; try { var _result; result = test.call(ctx, value, ctx); if (typeof ((_result = result) == null ? void 0 : _result.then) === 'function') { if (options.sync) { throw new Error(`Validation test of type: "${ctx.type}" returned a Promise during a synchronous validate. ` + `This test will finish after the validate call has returned`); } return Promise.resolve(result).then(handleResult, handleError); } } catch (err) { handleError(err); return; } handleResult(result); } validate.OPTIONS = config; return validate; } function getIn(schema, path, value, context = value) { let parent, lastPart, lastPartDebug; // root path: '' if (!path) return { parent, parentPath: path, schema }; propertyExpr.forEach(path, (_part, isBracket, isArray) => { let part = isBracket ? _part.slice(1, _part.length - 1) : _part; schema = schema.resolve({ context, parent, value }); let isTuple = schema.type === 'tuple'; let idx = isArray ? parseInt(part, 10) : 0; if (schema.innerType || isTuple) { if (isTuple && !isArray) throw new Error(`Yup.reach cannot implicitly index into a tuple type. the path part "${lastPartDebug}" must contain an index to the tuple element, e.g. "${lastPartDebug}[0]"`); if (value && idx >= value.length) { throw new Error(`Yup.reach cannot resolve an array item at index: ${_part}, in the path: ${path}. ` + `because there is no value at that index. `); } parent = value; value = value && value[idx]; schema = isTuple ? schema.spec.types[idx] : schema.innerType; } // sometimes the array index part of a path doesn't exist: "nested.arr.child" // in these cases the current part is the next schema and should be processed // in this iteration. For cases where the index signature is included this // check will fail and we'll handle the `child` part on the next iteration like normal if (!isArray) { if (!schema.fields || !schema.fields[part]) throw new Error(`The schema does not contain the path: ${path}. ` + `(failed at: ${lastPartDebug} which is a type: "${schema.type}")`); parent = value; value = value && value[part]; schema = schema.fields[part]; } lastPart = part; lastPartDebug = isBracket ? '[' + _part + ']' : '.' + _part; }); return { schema, parent, parentPath: lastPart }; } function reach(obj, path, value, context) { return getIn(obj, path, value, context).schema; } class ReferenceSet extends Set { describe() { const description = []; for (const item of this.values()) { description.push(Reference.isRef(item) ? item.describe() : item); } return description; } resolveAll(resolve) { let result = []; for (const item of this.values()) { result.push(resolve(item)); } return result; } clone() { return new ReferenceSet(this.values()); } merge(newItems, removeItems) { const next = this.clone(); newItems.forEach(value => next.add(value)); removeItems.forEach(value => next.delete(value)); return next; } } // tweaked from https://github.com/Kelin2025/nanoclone/blob/0abeb7635bda9b68ef2277093f76dbe3bf3948e1/src/index.js function clone(src, seen = new Map()) { if (isSchema(src) || !src || typeof src !== 'object') return src; if (seen.has(src)) return seen.get(src); let copy; if (src instanceof Date) { // Date copy = new Date(src.getTime()); seen.set(src, copy); } else if (src instanceof RegExp) { // RegExp copy = new RegExp(src); seen.set(src, copy); } else if (Array.isArray(src)) { // Array copy = new Array(src.length); seen.set(src, copy); for (let i = 0; i < src.length; i++) copy[i] = clone(src[i], seen); } else if (src instanceof Map) { // Map copy = new Map(); seen.set(src, copy); for (const [k, v] of src.entries()) copy.set(k, clone(v, seen)); } else if (src instanceof Set) { // Set copy = new Set(); seen.set(src, copy); for (const v of src) copy.add(clone(v, seen)); } else if (src instanceof Object) { // Object copy = {}; seen.set(src, copy); for (const [k, v] of Object.entries(src)) copy[k] = clone(v, seen); } else { throw Error(`Unable to clone ${src}`); } return copy; } // If `CustomSchemaMeta` isn't extended with any keys, we'll fall back to a // loose Record definition allowing free form usage. class Schema { constructor(options) { this.type = void 0; this.deps = []; this.tests = void 0; this.transforms = void 0; this.conditions = []; this._mutate = void 0; this.internalTests = {}; this._whitelist = new ReferenceSet(); this._blacklist = new ReferenceSet(); this.exclusiveTests = Object.create(null); this._typeCheck = void 0; this.spec = void 0; this.tests = []; this.transforms = []; this.withMutation(() => { this.typeError(mixed.notType); }); this.type = options.type; this._typeCheck = options.check; this.spec = Object.assign({ strip: false, strict: false, abortEarly: true, recursive: true, disableStackTrace: false, nullable: false, optional: true, coerce: true }, options == null ? void 0 : options.spec); this.withMutation(s => { s.nonNullable(); }); } // TODO: remove get _type() { return this.type; } clone(spec) { if (this._mutate) { if (spec) Object.assign(this.spec, spec); return this; } // if the nested value is a schema we can skip cloning, since // they are already immutable const next = Object.create(Object.getPrototypeOf(this)); // @ts-expect-error this is readonly next.type = this.type; next._typeCheck = this._typeCheck; next._whitelist = this._whitelist.clone(); next._blacklist = this._blacklist.clone(); next.internalTests = Object.assign({}, this.internalTests); next.exclusiveTests = Object.assign({}, this.exclusiveTests); // @ts-expect-error this is readonly next.deps = [...this.deps]; next.conditions = [...this.conditions]; next.tests = [...this.tests]; next.transforms = [...this.transforms]; next.spec = clone(Object.assign({}, this.spec, spec)); return next; } label(label) { let next = this.clone(); next.spec.label = label; return next; } meta(...args) { if (args.length === 0) return this.spec.meta; let next = this.clone(); next.spec.meta = Object.assign(next.spec.meta || {}, args[0]); return next; } withMutation(fn) { let before = this._mutate; this._mutate = true; let result = fn(this); this._mutate = before; return result; } concat(schema) { if (!schema || schema === this) return this; if (schema.type !== this.type && this.type !== 'mixed') throw new TypeError(`You cannot \`concat()\` schema's of different types: ${this.type} and ${schema.type}`); let base = this; let combined = schema.clone(); const mergedSpec = Object.assign({}, base.spec, combined.spec); combined.spec = mergedSpec; combined.internalTests = Object.assign({}, base.internalTests, combined.internalTests); // manually merge the blacklist/whitelist (the other `schema` takes // precedence in case of conflicts) combined._whitelist = base._whitelist.merge(schema._whitelist, schema._blacklist); combined._blacklist = base._blacklist.merge(schema._blacklist, schema._whitelist); // start with the current tests combined.tests = base.tests; combined.exclusiveTests = base.exclusiveTests; // manually add the new tests to ensure // the deduping logic is consistent combined.withMutation(next => { schema.tests.forEach(fn => { next.test(fn.OPTIONS); }); }); combined.transforms = [...base.transforms, ...combined.transforms]; return combined; } isType(v) { if (v == null) { if (this.spec.nullable && v === null) return true; if (this.spec.optional && v === undefined) return true; return false; } return this._typeCheck(v); } resolve(options) { let schema = this; if (schema.conditions.length) { let conditions = schema.conditions; schema = schema.clone(); schema.conditions = []; schema = conditions.reduce((prevSchema, condition) => condition.resolve(prevSchema, options), schema); schema = schema.resolve(options); } return schema; } resolveOptions(options) { var _options$strict, _options$abortEarly, _options$recursive, _options$disableStack; return Object.assign({}, options, { from: options.from || [], strict: (_options$strict = options.strict) != null ? _options$strict : this.spec.strict, abortEarly: (_options$abortEarly = options.abortEarly) != null ? _options$abortEarly : this.spec.abortEarly, recursive: (_options$recursive = options.recursive) != null ? _options$recursive : this.spec.recursive, disableStackTrace: (_options$disableStack = options.disableStackTrace) != null ? _options$disableStack : this.spec.disableStackTrace }); } /** * Run the configured transform pipeline over an input value. */ cast(value, options = {}) { let resolvedSchema = this.resolve(Object.assign({ value }, options)); let allowOptionality = options.assert === 'ignore-optionality'; let result = resolvedSchema._cast(value, options); if (options.assert !== false && !resolvedSchema.isType(result)) { if (allowOptionality && isAbsent(result)) { return result; } let formattedValue = printValue(value); let formattedResult = printValue(result); throw new TypeError(`The value of ${options.path || 'field'} could not be cast to a value ` + `that satisfies the schema type: "${resolvedSchema.type}". \n\n` + `attempted value: ${formattedValue} \n` + (formattedResult !== formattedValue ? `result of cast: ${formattedResult}` : '')); } return result; } _cast(rawValue, options) { let value = rawValue === undefined ? rawValue : this.transforms.reduce((prevValue, fn) => fn.call(this, prevValue, rawValue, this), rawValue); if (value === undefined) { value = this.getDefault(options); } return value; } _validate(_value, options = {}, panic, next) { let { path, originalValue = _value, strict = this.spec.strict } = options; let value = _value; if (!strict) { value = this._cast(value, Object.assign({ assert: false }, options)); } let initialTests = []; for (let test of Object.values(this.internalTests)) { if (test) initialTests.push(test); } this.runTests({ path, value, originalValue, options, tests: initialTests }, panic, initialErrors => { // even if we aren't ending early we can't proceed further if the types aren't correct if (initialErrors.length) { return next(initialErrors, value); } this.runTests({ path, value, originalValue, options, tests: this.tests }, panic, next); }); } /** * Executes a set of validations, either schema, produced Tests or a nested * schema validate result. */ runTests(runOptions, panic, next) { let fired = false; let { tests, value, originalValue, path, options } = runOptions; let panicOnce = arg => { if (fired) return; fired = true; panic(arg, value); }; let nextOnce = arg => { if (fired) return; fired = true; next(arg, value); }; let count = tests.length; let nestedErrors = []; if (!count) return nextOnce([]); let args = { value, originalValue, path, options, schema: this }; for (let i = 0; i < tests.length; i++) { const test = tests[i]; test(args, panicOnce, function finishTestRun(err) { if (err) { Array.isArray(err) ? nestedErrors.push(...err) : nestedErrors.push(err); } if (--count <= 0) { nextOnce(nestedErrors); } }); } } asNestedTest({ key, index, parent, parentPath, originalParent, options }) { const k = key != null ? key : index; if (k == null) { throw TypeError('Must include `key` or `index` for nested validations'); } const isIndex = typeof k === 'number'; let value = parent[k]; const testOptions = Object.assign({}, options, { // Nested validations fields are always strict: // 1. parent isn't strict so the casting will also have cast inner values // 2. parent is strict in which case the nested values weren't cast either strict: true, parent, value, originalValue: originalParent[k], // FIXME: tests depend on `index` being passed around deeply, // we should not let the options.key/index bleed through key: undefined, // index: undefined, [isIndex ? 'index' : 'key']: k, path: isIndex || k.includes('.') ? `${parentPath || ''}[${isIndex ? k : `"${k}"`}]` : (parentPath ? `${parentPath}.` : '') + key }); return (_, panic, next) => this.resolve(testOptions)._validate(value, testOptions, panic, next); } validate(value, options) { var _options$disableStack2; let schema = this.resolve(Object.assign({}, options, { value })); let disableStackTrace = (_options$disableStack2 = options == null ? void 0 : options.disableStackTrace) != null ? _options$disableStack2 : schema.spec.disableStackTrace; return new Promise((resolve, reject) => schema._validate(value, options, (error, parsed) => { if (ValidationError.isError(error)) error.value = parsed; reject(error); }, (errors, validated) => { if (errors.length) reject(new ValidationError(errors, validated, undefined, undefined, disableStackTrace));else resolve(validated); })); } validateSync(value, options) { var _options$disableStack3; let schema = this.resolve(Object.assign({}, options, { value })); let result; let disableStackTrace = (_options$disableStack3 = options == null ? void 0 : options.disableStackTrace) != null ? _options$disableStack3 : schema.spec.disableStackTrace; schema._validate(value, Object.assign({}, options, { sync: true }), (error, parsed) => { if (ValidationError.isError(error)) error.value = parsed; throw error; }, (errors, validated) => { if (errors.length) throw new ValidationError(errors, value, undefined, undefined, disableStackTrace); result = validated; }); return result; } isValid(value, options) { return this.validate(value, options).then(() => true, err => { if (ValidationError.isError(err)) return false; throw err; }); } isValidSync(value, options) { try { this.validateSync(value, options); return true; } catch (err) { if (ValidationError.isError(err)) return false; throw err; } } _getDefault(options) { let defaultValue = this.spec.default; if (defaultValue == null) { return defaultValue; } return typeof defaultValue === 'function' ? defaultValue.call(this, options) : clone(defaultValue); } getDefault(options // If schema is defaulted we know it's at least not undefined ) { let schema = this.resolve(options || {}); return schema._getDefault(options); } default(def) { if (arguments.length === 0) { return this._getDefault(); } let next = this.clone({ default: def }); return next; } strict(isStrict = true) { return this.clone({ strict: isStrict }); } nullability(nullable, message) { const next = this.clone({ nullable }); next.internalTests.nullable = createValidation({ message, name: 'nullable', test(value) { return value === null ? this.schema.spec.nullable : true; } }); return next; } optionality(optional, message) { const next = this.clone({ optional }); next.internalTests.optionality = createValidation({ message, name: 'optionality', test(value) { return value === undefined ? this.schema.spec.optional : true; } }); return next; } optional() { return this.optionality(true); } defined(message = mixed.defined) { return this.optionality(false, message); } nullable() { return this.nullability(true); } nonNullable(message = mixed.notNull) { return this.nullability(false, message); } required(message = mixed.required) { return this.clone().withMutation(next => next.nonNullable(message).defined(message)); } notRequired() { return this.clone().withMutation(next => next.nullable().optional()); } transform(fn) { let next = this.clone(); next.transforms.push(fn); 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(...args) { let opts; if (args.length === 1) { if (typeof args[0] === 'function') { opts = { test: args[0] }; } else { opts = args[0]; } } else if (args.length === 2) { opts = { name: args[0], test: args[1] }; } else { opts = { name: args[0], message: args[1], test: args[2] }; } if (opts.message === undefined) opts.message = mixed.default; if (typeof opts.test !== 'function') throw new TypeError('`test` is a required parameters'); let next = this.clone(); let validate = createValidation(opts); let isExclusive = opts.exclusive || opts.name && next.exclusiveTests[opts.name] === true; if (opts.exclusive) { if (!opts.name) throw new TypeError('Exclusive tests must provide a unique `name` identifying the test'); } if (opts.name) next.exclusiveTests[opts.name] = !!opts.exclusive; next.tests = next.tests.filter(fn => { if (fn.OPTIONS.name === opts.name) { if (isExclusive) return false; if (fn.OPTIONS.test === validate.OPTIONS.test) return false; } return true; }); next.tests.push(validate); return next; } when(keys, options) { if (!Array.isArray(keys) && typeof keys !== 'string') { options = keys; keys = '.'; } let next = this.clone(); let deps = toArray(keys).map(key => new Reference(key)); deps.forEach(dep => { // @ts-ignore readonly array if (dep.isSibling) next.deps.push(dep.key); }); next.conditions.push(typeof options === 'function' ? new Condition(deps, options) : Condition.fromOptions(deps, options)); return next; } typeError(message) { let next = this.clone(); next.internalTests.typeError = createValidation({ message, name: 'typeError', skipAbsent: true, test(value) { if (!this.schema._typeCheck(value)) return this.createError({ params: { type: this.schema.type } }); return true; } }); return next; } oneOf(enums, message = mixed.oneOf) { let next = this.clone(); enums.forEach(val => { next._whitelist.add(val); next._blacklist.delete(val); }); next.internalTests.whiteList = createValidation({ message, name: 'oneOf', skipAbsent: true, test(value) { let valids = this.schema._whitelist; let resolved = valids.resolveAll(this.resolve); return resolved.includes(value) ? true : this.createError({ params: { values: Array.from(valids).join(', '), resolved } }); } }); return next; } notOneOf(enums, message = mixed.notOneOf) { let next = this.clone(); enums.forEach(val => { next._blacklist.add(val); next._whitelist.delete(val); }); next.internalTests.blacklist = createValidation({ message, name: 'notOneOf', test(value) { let invalids = this.schema._blacklist; let resolved = invalids.resolveAll(this.resolve); if (resolved.includes(value)) return this.createError({ params: { values: Array.from(invalids).join(', '), resolved } }); return true; } }); return next; } strip(strip = true) { let next = this.clone(); next.spec.strip = strip; return next; } /** * Return a serialized description of the schema including validations, flags, types etc. * * @param options Provide any needed context for resolving runtime schema alterations (lazy, when conditions, etc). */ describe(options) { const next = (options ? this.resolve(options) : this).clone(); const { label, meta, optional, nullable } = next.spec; const description = { meta, label, optional, nullable, default: next.getDefault(options), type: next.type, oneOf: next._whitelist.describe(), notOneOf: next._blacklist.describe(), tests: next.tests.map(fn => ({ name: fn.OPTIONS.name, params: fn.OPTIONS.params })).filter((n, idx, list) => list.findIndex(c => c.name === n.name) === idx) }; return description; } } // @ts-expect-error Schema.prototype.__isYupSchema__ = true; for (const method of ['validate', 'validateSync']) Schema.prototype[`${method}At`] = function (path, value, options = {}) { const { parent, parentPath, schema } = getIn(this, path, value, options.context); return schema[method](parent && parent[parentPath], Object.assign({}, options, { parent, path })); }; for (const alias of ['equals', 'is']) Schema.prototype[alias] = Schema.prototype.oneOf; for (const alias of ['not', 'nope']) Schema.prototype[alias] = Schema.prototype.notOneOf; const returnsTrue = () => true; function create$8(spec) { return new MixedSchema(spec); } class MixedSchema extends Schema { constructor(spec) { super(typeof spec === 'function' ? { type: 'mixed', check: spec } : Object.assign({ type: 'mixed', check: returnsTrue }, spec)); } } create$8.prototype = MixedSchema.prototype; function create$7() { return new BooleanSchema(); } class BooleanSchema extends Schema { constructor() { super({ type: 'boolean', check(v) { if (v instanceof Boolean) v = v.valueOf(); return typeof v === 'boolean'; } }); this.withMutation(() => { this.transform((value, _raw, ctx) => { if (ctx.spec.coerce && !ctx.isType(value)) { if (/^(true|1)$/i.test(String(value))) return true; if (/^(false|0)$/i.test(String(value))) return false; } return value; }); }); } isTrue(message = boolean.isValue) { return this.test({ message, name: 'is-value', exclusive: true, params: { value: 'true' }, test(value) { return isAbsent(value) || value === true; } }); } isFalse(message = boolean.isValue) { return this.test({ message, name: 'is-value', exclusive: true, params: { value: 'false' }, test(value) { return isAbsent(value) || value === false; } }); } default(def) { return super.default(def); } defined(msg) { return super.defined(msg); } optional() { return super.optional(); } required(msg) { return super.required(msg); } notRequired() { return super.notRequired(); } nullable() { return super.nullable(); } nonNullable(msg) { return super.nonNullable(msg); } strip(v) { return super.strip(v); } } create$7.prototype = BooleanSchema.prototype; /** * This file is a modified version of the file from the following repository: * Date.parse with progressive enhancement for ISO 8601 <https://github.com/csnover/js-iso8601> * NON-CONFORMANT EDITION. * © 2011 Colin Snover <http://zetafleet.com> * Released under MIT license. */ // prettier-ignore // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm const isoReg = /^(\d{4}|[+-]\d{6})(?:-?(\d{2})(?:-?(\d{2}))?)?(?:[ T]?(\d{2}):?(\d{2})(?::?(\d{2})(?:[,.](\d{1,}))?)?(?:(Z)|([+-])(\d{2})(?::?(\d{2}))?)?)?$/; function parseIsoDate(date) { const struct = parseDateStruct(date); if (!struct) return Date.parse ? Date.parse(date) : Number.NaN; // timestamps without timezone identifiers should be considered local time if (struct.z === undefined && struct.plusMinus === undefined) { return new Date(struct.year, struct.month, struct.day, struct.hour, struct.minute, struct.second, struct.millisecond).valueOf(); } let totalMinutesOffset = 0; if (struct.z !== 'Z' && struct.plusMinus !== undefined) { totalMinutesOffset = struct.hourOffset * 60 + struct.minuteOffset; if (struct.plusMinus === '+') totalMinutesOffset = 0 - totalMinutesOffset; } return Date.UTC(struct.year, struct.month, struct.day, struct.hour, struct.minute + totalMinutesOffset, struct.second, struct.millisecond); } function parseDateStruct(date) { var _regexResult$7$length, _regexResult$; const regexResult = isoReg.exec(date); if (!regexResult) return null; // use of toNumber() avoids NaN timestamps caused by “undefined” // values being passed to Date constructor return { year: toNumber(regexResult[1]), month: toNumber(regexResult[2], 1) - 1, day: toNumber(regexResult[3], 1), hour: toNumber(regexResult[4]), minute: toNumber(regexResult[5]), second: toNumber(regexResult[6]), millisecond: regexResult[7] ? // allow arbitrary sub-second precision beyond milliseconds toNumber(regexResult[7].substring(0, 3)) : 0, precision: (_regexResult$7$length = (_regexResult$ = regexResult[7]) == null ? void 0 : _regexResult$.length) != null ? _regexResult$7$length : undefined, z: regexResult[8] || undefined, plusMinus: regexResult[9] || undefined, hourOffset: toNumber(regexResult[10]), minuteOffset: toNumber(regexResult[11]) }; } function toNumber(str, defaultValue = 0) { return Number(str) || defaultValue; } // Taken from HTML spec: https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address let rEmail = // eslint-disable-next-line /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; let rUrl = // eslint-disable-next-line /^((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; // eslint-disable-next-line let rUUID = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; let yearMonthDay = '^\\d{4}-\\d{2}-\\d{2}'; let hourMinuteSecond = '\\d{2}:\\d{2}:\\d{2}'; let zOrOffset = '(([+-]\\d{2}(:?\\d{2})?)|Z)'; let rIsoDateTime = new RegExp(`${yearMonthDay}T${hourMinuteSecond}(\\.\\d+)?${zOrOffset}$`); let isTrimmed = value => isAbsent(value) || value === value.trim(); let objStringTag = {}.toString(); function create$6() { return new StringSchema(); } class StringSchema extends Schema { constructor() { super({ type: 'string', check(value) { if (value instanceof String) value = value.valueOf(); return typeof value === 'string'; } }); this.withMutation(() => { this.transform((value, _raw, ctx) => { if (!ctx.spec.coerce || ctx.isType(value)) return value; // don't ever convert arrays if (Array.isArray(value)) return value; const strValue = value != null && value.toString ? value.toString() : value; // no one wants plain objects converted to [Object object] if (strValue === objStringTag) return value; return strValue; }); }); } required(message) { return super.required(message).withMutation(schema => schema.test({ message: message || mixed.required, name: 'required', skipAbsent: true, test: value => !!value.length })); } notRequired() { return super.notRequired().withMutation(schema => { schema.tests = schema.tests.filter(t => t.OPTIONS.name !== 'required'); return schema; }); } length(length, message = string.length) { return this.test({ message, name: 'length', exclusive: true, params: { length }, skipAbsent: true, test(value) { return value.length === this.resolve(length); } }); } min(min, message = string.min) { return this.test({ message, name: 'min', exclusive: true, params: { min }, skipAbsent: true, test(value) { return value.length >= this.resolve(min); } }); } max(max, message = string.max) { return this.test({ name: 'max', exclusive: true, message, params: { max }, skipAbsent: true, test(value) { return value.length <= this.resolve(max); } }); } matches(regex, options) { let excludeEmptyString = false; let message; let name; if (options) { if (typeof options === 'object') { ({ excludeEmptyString = false, message, name } = options); } else { message = options; } } return this.test({ name: name || 'matches', message: message || string.matches, params: { regex }, skipAbsent: true, test: value => value === '' && excludeEmptyString || value.search(regex) !== -1 }); } email(message = string.email) { return this.matches(rEmail, { name: 'email', message, excludeEmptyString: true }); } url(message = string.url) { return this.matches(rUrl, { name: 'url', message, excludeEmptyString: true }); } uuid(message = string.uuid) { return this.matches(rUUID, { name: 'uuid', message, excludeEmptyString: false }); } datetime(options) { let message = ''; let allowOffset; let precision; if (options) { if (typeof options === 'object') { ({ message = '', allowOffset = false, precision = undefined } = options); } else { message = options; } } return this.matches(rIsoDateTime, { name: 'datetime', message: message || string.datetime, excludeEmptyString: true }).test({ name: 'datetime_offset', message: message || string.datetime_offset, params: { allowOffset }, skipAbsent: true, test: value => { if (!value || allowOffset) return true; const struct = parseDateStruct(value); if (!struct) return false; return !!struct.z; } }).test({ name: 'datetime_precision', message: message || string.datetime_precision, params: { precision }, skipAbsent: true, test: value => { if (!value || precision == undefined) return true; const struct = parseDateStruct(value); if (!struct) return false; return struct.precision === precision; } }); } //-- transforms -- ensure() { return this.default('').transform(val => val === null ? '' : val); } trim(message = string.trim) { return this.transform(val => val != null ? val.trim() : val).test({ message, name: 'trim', test: isTrimmed }); } lowercase(message = string.lowercase) { return this.transform(value => !isAbsent(value) ? value.toLowerCase() : value).test({ message, name: 'string_case', exclusive: true, skipAbsent: true, test: value => isAbsent(value) || value === value.toLowerCase() }); } uppercase(message = string.uppercase) { return this.transform(value => !isAbsent(value) ? value.toUpperCase() : value).test({ message, name: 'string_case', exclusive: true, skipAbsent: true, test: value => isAbsent(value) || value === value.toUpperCase() }); } } create$6.prototype = StringSchema.prototype; // // String Interfaces // let isNaN$1 = value => value != +value; function create$5() { return new NumberSchema(); } class NumberSchema extends Schema { constructor() { super({ type: 'number', check(value) { if (value instanceof Number) value = value.valueOf(); return typeof value === 'number' && !isNaN$1(value); } }); this.withMutation(() => { this.transform((value, _raw, ctx) => { if (!ctx.spec.coerce) return value; let parsed = value; if (typeof parsed === 'string') { parsed = parsed.replace(/\s/g, ''); if (parsed === '') return NaN; // don't use parseFloat to avoid positives on alpha-numeric strings parsed = +parsed; } // null -> NaN isn't useful; treat all nulls as null and let it fail on // nullability check vs TypeErrors if (ctx.isType(parsed) || parsed === null) return parsed; return parseFloat(parsed); }); }); } min(min, message = number.min) { return this.test({ message, name: 'min', exclusive: true, params: { min }, skipAbsent: true, test(value) { return value >= this.resolve(min); } }); } max(max, message = number.max) { return this.test({ message, name: 'max', exclusive: true, params: { max }, skipAbsent: true, test(value) { return value <= this.resolve(max); } }); } lessThan(less, message = number.lessThan) { return this.test({ message, name: 'max', exclusive: true, params: { less }, skipAbsent: true, test(value) { return value < this.resolve(less); } }); } moreThan(more, message = number.moreThan) { return this.test({ message, name: 'min', exclusive: true, params: { more }, skipAbsent: true, test(value) { return value > this.resolve(more); } }); } positive(msg = number.positive) { return this.moreThan(0, msg); } negative(msg = number.negative) { return this.lessThan(0, msg); } integer(message = number.integer) { return this.test({ name: 'integer', message, skipAbsent: true, test: val => Number.isInteger(val) }); } truncate() { return this.transform(value => !isAbsent(value) ? value | 0 : value); } round(method) { var _method; let avail = ['ceil', 'floor', 'round', 'trunc']; method = ((_method = method) == null ? void 0 : _method.toLowerCase()) || 'round'; // this exists for symemtry with the new Math.trunc if (method === 'trunc') return this.truncate(); if (avail.indexOf(method.toLowerCase()) === -1) throw new TypeError('Only valid options for round() are: ' + avail.join