560 lines
14 KiB
JavaScript
560 lines
14 KiB
JavaScript
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
|
|
// @ts-ignore
|
|
import cloneDeep from 'nanoclone';
|
|
import { mixed as locale } from './locale';
|
|
import Condition from './Condition';
|
|
import runTests from './util/runTests';
|
|
import createValidation from './util/createValidation';
|
|
import printValue from './util/printValue';
|
|
import Ref from './Reference';
|
|
import { getIn } from './util/reach';
|
|
import toArray from './util/toArray';
|
|
import ValidationError from './ValidationError';
|
|
import ReferenceSet from './util/ReferenceSet';
|
|
export default class BaseSchema {
|
|
constructor(options) {
|
|
this.deps = [];
|
|
this.conditions = [];
|
|
this._whitelist = new ReferenceSet();
|
|
this._blacklist = new ReferenceSet();
|
|
this.exclusiveTests = Object.create(null);
|
|
this.tests = [];
|
|
this.transforms = [];
|
|
this.withMutation(() => {
|
|
this.typeError(locale.notType);
|
|
});
|
|
this.type = (options == null ? void 0 : options.type) || 'mixed';
|
|
this.spec = _extends({
|
|
strip: false,
|
|
strict: false,
|
|
abortEarly: true,
|
|
recursive: true,
|
|
nullable: false,
|
|
presence: 'optional'
|
|
}, options == null ? void 0 : options.spec);
|
|
} // TODO: remove
|
|
|
|
|
|
get _type() {
|
|
return this.type;
|
|
}
|
|
|
|
_typeCheck(_value) {
|
|
return true;
|
|
}
|
|
|
|
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._typeError = this._typeError;
|
|
next._whitelistError = this._whitelistError;
|
|
next._blacklistError = this._blacklistError;
|
|
next._whitelist = this._whitelist.clone();
|
|
next._blacklist = this._blacklist.clone();
|
|
next.exclusiveTests = _extends({}, 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 = cloneDeep(_extends({}, this.spec, spec));
|
|
return next;
|
|
}
|
|
|
|
label(label) {
|
|
var 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;
|
|
} // withContext<TContext extends AnyObject>(): BaseSchema<
|
|
// TCast,
|
|
// TContext,
|
|
// TOutput
|
|
// > {
|
|
// return this as any;
|
|
// }
|
|
|
|
|
|
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 = _extends({}, base.spec, combined.spec); // if (combined.spec.nullable === UNSET)
|
|
// mergedSpec.nullable = base.spec.nullable;
|
|
// if (combined.spec.presence === UNSET)
|
|
// mergedSpec.presence = base.spec.presence;
|
|
|
|
|
|
combined.spec = mergedSpec;
|
|
combined._typeError || (combined._typeError = base._typeError);
|
|
combined._whitelistError || (combined._whitelistError = base._whitelistError);
|
|
combined._blacklistError || (combined._blacklistError = base._blacklistError); // 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);
|
|
});
|
|
});
|
|
return combined;
|
|
}
|
|
|
|
isType(v) {
|
|
if (this.spec.nullable && v === null) return true;
|
|
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((schema, condition) => condition.resolve(schema, options), schema);
|
|
schema = schema.resolve(options);
|
|
}
|
|
|
|
return schema;
|
|
}
|
|
/**
|
|
*
|
|
* @param {*} value
|
|
* @param {Object} options
|
|
* @param {*=} options.parent
|
|
* @param {*=} options.context
|
|
*/
|
|
|
|
|
|
cast(value, options = {}) {
|
|
let resolvedSchema = this.resolve(_extends({
|
|
value
|
|
}, options));
|
|
|
|
let result = resolvedSchema._cast(value, options);
|
|
|
|
if (value !== undefined && options.assert !== false && resolvedSchema.isType(result) !== true) {
|
|
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((value, fn) => fn.call(this, value, rawValue, this), rawValue);
|
|
|
|
if (value === undefined) {
|
|
value = this.getDefault();
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
_validate(_value, options = {}, cb) {
|
|
let {
|
|
sync,
|
|
path,
|
|
from = [],
|
|
originalValue = _value,
|
|
strict = this.spec.strict,
|
|
abortEarly = this.spec.abortEarly
|
|
} = options;
|
|
let value = _value;
|
|
|
|
if (!strict) {
|
|
// this._validating = true;
|
|
value = this._cast(value, _extends({
|
|
assert: false
|
|
}, options)); // this._validating = false;
|
|
} // value is cast, we can check if it meets type requirements
|
|
|
|
|
|
let args = {
|
|
value,
|
|
path,
|
|
options,
|
|
originalValue,
|
|
schema: this,
|
|
label: this.spec.label,
|
|
sync,
|
|
from
|
|
};
|
|
let initialTests = [];
|
|
if (this._typeError) initialTests.push(this._typeError);
|
|
if (this._whitelistError) initialTests.push(this._whitelistError);
|
|
if (this._blacklistError) initialTests.push(this._blacklistError);
|
|
runTests({
|
|
args,
|
|
value,
|
|
path,
|
|
sync,
|
|
tests: initialTests,
|
|
endEarly: abortEarly
|
|
}, err => {
|
|
if (err) return void cb(err, value);
|
|
runTests({
|
|
tests: this.tests,
|
|
args,
|
|
path,
|
|
sync,
|
|
value,
|
|
endEarly: abortEarly
|
|
}, cb);
|
|
});
|
|
}
|
|
|
|
validate(value, options, maybeCb) {
|
|
let schema = this.resolve(_extends({}, options, {
|
|
value
|
|
})); // callback case is for nested validations
|
|
|
|
return typeof maybeCb === 'function' ? schema._validate(value, options, maybeCb) : new Promise((resolve, reject) => schema._validate(value, options, (err, value) => {
|
|
if (err) reject(err);else resolve(value);
|
|
}));
|
|
}
|
|
|
|
validateSync(value, options) {
|
|
let schema = this.resolve(_extends({}, options, {
|
|
value
|
|
}));
|
|
let result;
|
|
|
|
schema._validate(value, _extends({}, options, {
|
|
sync: true
|
|
}), (err, value) => {
|
|
if (err) throw err;
|
|
result = value;
|
|
});
|
|
|
|
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() {
|
|
let defaultValue = this.spec.default;
|
|
|
|
if (defaultValue == null) {
|
|
return defaultValue;
|
|
}
|
|
|
|
return typeof defaultValue === 'function' ? defaultValue.call(this) : cloneDeep(defaultValue);
|
|
}
|
|
|
|
getDefault(options) {
|
|
let schema = this.resolve(options || {});
|
|
return schema._getDefault();
|
|
}
|
|
|
|
default(def) {
|
|
if (arguments.length === 0) {
|
|
return this._getDefault();
|
|
}
|
|
|
|
let next = this.clone({
|
|
default: def
|
|
});
|
|
return next;
|
|
}
|
|
|
|
strict(isStrict = true) {
|
|
var next = this.clone();
|
|
next.spec.strict = isStrict;
|
|
return next;
|
|
}
|
|
|
|
_isPresent(value) {
|
|
return value != null;
|
|
}
|
|
|
|
defined(message = locale.defined) {
|
|
return this.test({
|
|
message,
|
|
name: 'defined',
|
|
exclusive: true,
|
|
|
|
test(value) {
|
|
return value !== undefined;
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
required(message = locale.required) {
|
|
return this.clone({
|
|
presence: 'required'
|
|
}).withMutation(s => s.test({
|
|
message,
|
|
name: 'required',
|
|
exclusive: true,
|
|
|
|
test(value) {
|
|
return this.schema._isPresent(value);
|
|
}
|
|
|
|
}));
|
|
}
|
|
|
|
notRequired() {
|
|
var next = this.clone({
|
|
presence: 'optional'
|
|
});
|
|
next.tests = next.tests.filter(test => test.OPTIONS.name !== 'required');
|
|
return next;
|
|
}
|
|
|
|
nullable(isNullable = true) {
|
|
var next = this.clone({
|
|
nullable: isNullable !== false
|
|
});
|
|
return next;
|
|
}
|
|
|
|
transform(fn) {
|
|
var 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 = locale.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 Ref(key));
|
|
deps.forEach(dep => {
|
|
// @ts-ignore
|
|
if (dep.isSibling) next.deps.push(dep.key);
|
|
});
|
|
next.conditions.push(new Condition(deps, options));
|
|
return next;
|
|
}
|
|
|
|
typeError(message) {
|
|
var next = this.clone();
|
|
next._typeError = createValidation({
|
|
message,
|
|
name: 'typeError',
|
|
|
|
test(value) {
|
|
if (value !== undefined && !this.schema.isType(value)) return this.createError({
|
|
params: {
|
|
type: this.schema._type
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
});
|
|
return next;
|
|
}
|
|
|
|
oneOf(enums, message = locale.oneOf) {
|
|
var next = this.clone();
|
|
enums.forEach(val => {
|
|
next._whitelist.add(val);
|
|
|
|
next._blacklist.delete(val);
|
|
});
|
|
next._whitelistError = createValidation({
|
|
message,
|
|
name: 'oneOf',
|
|
|
|
test(value) {
|
|
if (value === undefined) return true;
|
|
let valids = this.schema._whitelist;
|
|
return valids.has(value, this.resolve) ? true : this.createError({
|
|
params: {
|
|
values: valids.toArray().join(', ')
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
return next;
|
|
}
|
|
|
|
notOneOf(enums, message = locale.notOneOf) {
|
|
var next = this.clone();
|
|
enums.forEach(val => {
|
|
next._blacklist.add(val);
|
|
|
|
next._whitelist.delete(val);
|
|
});
|
|
next._blacklistError = createValidation({
|
|
message,
|
|
name: 'notOneOf',
|
|
|
|
test(value) {
|
|
let invalids = this.schema._blacklist;
|
|
if (invalids.has(value, this.resolve)) return this.createError({
|
|
params: {
|
|
values: invalids.toArray().join(', ')
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
});
|
|
return next;
|
|
}
|
|
|
|
strip(strip = true) {
|
|
let next = this.clone();
|
|
next.spec.strip = strip;
|
|
return next;
|
|
}
|
|
|
|
describe() {
|
|
const next = this.clone();
|
|
const {
|
|
label,
|
|
meta
|
|
} = next.spec;
|
|
const description = {
|
|
meta,
|
|
label,
|
|
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
|
|
BaseSchema.prototype.__isYupSchema__ = true;
|
|
|
|
for (const method of ['validate', 'validateSync']) BaseSchema.prototype[`${method}At`] = function (path, value, options = {}) {
|
|
const {
|
|
parent,
|
|
parentPath,
|
|
schema
|
|
} = getIn(this, path, value, options.context);
|
|
return schema[method](parent && parent[parentPath], _extends({}, options, {
|
|
parent,
|
|
path
|
|
}));
|
|
};
|
|
|
|
for (const alias of ['equals', 'is']) BaseSchema.prototype[alias] = BaseSchema.prototype.oneOf;
|
|
|
|
for (const alias of ['not', 'nope']) BaseSchema.prototype[alias] = BaseSchema.prototype.notOneOf;
|
|
|
|
BaseSchema.prototype.optional = BaseSchema.prototype.notRequired; |