123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750 |
- 'use strict';
- const Assert = require('@hapi/hoek/lib/assert');
- const Clone = require('@hapi/hoek/lib/clone');
- const Ignore = require('@hapi/hoek/lib/ignore');
- const Reach = require('@hapi/hoek/lib/reach');
- const Common = require('./common');
- const Errors = require('./errors');
- const State = require('./state');
- const internals = {
- result: Symbol('result')
- };
- exports.entry = function (value, schema, prefs) {
- let settings = Common.defaults;
- if (prefs) {
- Assert(prefs.warnings === undefined, 'Cannot override warnings preference in synchronous validation');
- Assert(prefs.artifacts === undefined, 'Cannot override artifacts preference in synchronous validation');
- settings = Common.preferences(Common.defaults, prefs);
- }
- const result = internals.entry(value, schema, settings);
- Assert(!result.mainstay.externals.length, 'Schema with external rules must use validateAsync()');
- const outcome = { value: result.value };
- if (result.error) {
- outcome.error = result.error;
- }
- if (result.mainstay.warnings.length) {
- outcome.warning = Errors.details(result.mainstay.warnings);
- }
- if (result.mainstay.debug) {
- outcome.debug = result.mainstay.debug;
- }
- if (result.mainstay.artifacts) {
- outcome.artifacts = result.mainstay.artifacts;
- }
- return outcome;
- };
- exports.entryAsync = async function (value, schema, prefs) {
- let settings = Common.defaults;
- if (prefs) {
- settings = Common.preferences(Common.defaults, prefs);
- }
- const result = internals.entry(value, schema, settings);
- const mainstay = result.mainstay;
- if (result.error) {
- if (mainstay.debug) {
- result.error.debug = mainstay.debug;
- }
- throw result.error;
- }
- if (mainstay.externals.length) {
- let root = result.value;
- const errors = [];
- for (const external of mainstay.externals) {
- const path = external.state.path;
- const linked = external.schema.type === 'link' ? mainstay.links.get(external.schema) : null;
- let node = root;
- let key;
- let parent;
- const ancestors = path.length ? [root] : [];
- const original = path.length ? Reach(value, path) : value;
- if (path.length) {
- key = path[path.length - 1];
- let current = root;
- for (const segment of path.slice(0, -1)) {
- current = current[segment];
- ancestors.unshift(current);
- }
- parent = ancestors[0];
- node = parent[key];
- }
- try {
- const createError = (code, local) => (linked || external.schema).$_createError(code, node, local, external.state, settings);
- const output = await external.method(node, {
- schema: external.schema,
- linked,
- state: external.state,
- prefs,
- original,
- error: createError,
- errorsArray: internals.errorsArray,
- warn: (code, local) => mainstay.warnings.push((linked || external.schema).$_createError(code, node, local, external.state, settings)),
- message: (messages, local) => (linked || external.schema).$_createError('external', node, local, external.state, settings, { messages })
- });
- if (output === undefined ||
- output === node) {
- continue;
- }
- if (output instanceof Errors.Report) {
- mainstay.tracer.log(external.schema, external.state, 'rule', 'external', 'error');
- errors.push(output);
- if (settings.abortEarly) {
- break;
- }
- continue;
- }
- if (Array.isArray(output) &&
- output[Common.symbols.errors]) {
- mainstay.tracer.log(external.schema, external.state, 'rule', 'external', 'error');
- errors.push(...output);
- if (settings.abortEarly) {
- break;
- }
- continue;
- }
- if (parent) {
- mainstay.tracer.value(external.state, 'rule', node, output, 'external');
- parent[key] = output;
- }
- else {
- mainstay.tracer.value(external.state, 'rule', root, output, 'external');
- root = output;
- }
- }
- catch (err) {
- if (settings.errors.label) {
- err.message += ` (${(external.label)})`; // Change message to include path
- }
- throw err;
- }
- }
- result.value = root;
- if (errors.length) {
- result.error = Errors.process(errors, value, settings);
- if (mainstay.debug) {
- result.error.debug = mainstay.debug;
- }
- throw result.error;
- }
- }
- if (!settings.warnings &&
- !settings.debug &&
- !settings.artifacts) {
- return result.value;
- }
- const outcome = { value: result.value };
- if (mainstay.warnings.length) {
- outcome.warning = Errors.details(mainstay.warnings);
- }
- if (mainstay.debug) {
- outcome.debug = mainstay.debug;
- }
- if (mainstay.artifacts) {
- outcome.artifacts = mainstay.artifacts;
- }
- return outcome;
- };
- internals.Mainstay = class {
- constructor(tracer, debug, links) {
- this.externals = [];
- this.warnings = [];
- this.tracer = tracer;
- this.debug = debug;
- this.links = links;
- this.shadow = null;
- this.artifacts = null;
- this._snapshots = [];
- }
- snapshot() {
- this._snapshots.push({
- externals: this.externals.slice(),
- warnings: this.warnings.slice()
- });
- }
- restore() {
- const snapshot = this._snapshots.pop();
- this.externals = snapshot.externals;
- this.warnings = snapshot.warnings;
- }
- commit() {
- this._snapshots.pop();
- }
- };
- internals.entry = function (value, schema, prefs) {
- // Prepare state
- const { tracer, cleanup } = internals.tracer(schema, prefs);
- const debug = prefs.debug ? [] : null;
- const links = schema._ids._schemaChain ? new Map() : null;
- const mainstay = new internals.Mainstay(tracer, debug, links);
- const schemas = schema._ids._schemaChain ? [{ schema }] : null;
- const state = new State([], [], { mainstay, schemas });
- // Validate value
- const result = exports.validate(value, schema, state, prefs);
- // Process value and errors
- if (cleanup) {
- schema.$_root.untrace();
- }
- const error = Errors.process(result.errors, value, prefs);
- return { value: result.value, error, mainstay };
- };
- internals.tracer = function (schema, prefs) {
- if (schema.$_root._tracer) {
- return { tracer: schema.$_root._tracer._register(schema) };
- }
- if (prefs.debug) {
- Assert(schema.$_root.trace, 'Debug mode not supported');
- return { tracer: schema.$_root.trace()._register(schema), cleanup: true };
- }
- return { tracer: internals.ignore };
- };
- exports.validate = function (value, schema, state, prefs, overrides = {}) {
- if (schema.$_terms.whens) {
- schema = schema._generate(value, state, prefs).schema;
- }
- // Setup state and settings
- if (schema._preferences) {
- prefs = internals.prefs(schema, prefs);
- }
- // Cache
- if (schema._cache &&
- prefs.cache) {
- const result = schema._cache.get(value);
- state.mainstay.tracer.debug(state, 'validate', 'cached', !!result);
- if (result) {
- return result;
- }
- }
- // Helpers
- const createError = (code, local, localState) => schema.$_createError(code, value, local, localState || state, prefs);
- const helpers = {
- original: value,
- prefs,
- schema,
- state,
- error: createError,
- errorsArray: internals.errorsArray,
- warn: (code, local, localState) => state.mainstay.warnings.push(createError(code, local, localState)),
- message: (messages, local) => schema.$_createError('custom', value, local, state, prefs, { messages })
- };
- // Prepare
- state.mainstay.tracer.entry(schema, state);
- const def = schema._definition;
- if (def.prepare &&
- value !== undefined &&
- prefs.convert) {
- const prepared = def.prepare(value, helpers);
- if (prepared) {
- state.mainstay.tracer.value(state, 'prepare', value, prepared.value);
- if (prepared.errors) {
- return internals.finalize(prepared.value, [].concat(prepared.errors), helpers); // Prepare error always aborts early
- }
- value = prepared.value;
- }
- }
- // Type coercion
- if (def.coerce &&
- value !== undefined &&
- prefs.convert &&
- (!def.coerce.from || def.coerce.from.includes(typeof value))) {
- const coerced = def.coerce.method(value, helpers);
- if (coerced) {
- state.mainstay.tracer.value(state, 'coerced', value, coerced.value);
- if (coerced.errors) {
- return internals.finalize(coerced.value, [].concat(coerced.errors), helpers); // Coerce error always aborts early
- }
- value = coerced.value;
- }
- }
- // Empty value
- const empty = schema._flags.empty;
- if (empty &&
- empty.$_match(internals.trim(value, schema), state.nest(empty), Common.defaults)) {
- state.mainstay.tracer.value(state, 'empty', value, undefined);
- value = undefined;
- }
- // Presence requirements (required, optional, forbidden)
- const presence = overrides.presence || schema._flags.presence || (schema._flags._endedSwitch ? null : prefs.presence);
- if (value === undefined) {
- if (presence === 'forbidden') {
- return internals.finalize(value, null, helpers);
- }
- if (presence === 'required') {
- return internals.finalize(value, [schema.$_createError('any.required', value, null, state, prefs)], helpers);
- }
- if (presence === 'optional') {
- if (schema._flags.default !== Common.symbols.deepDefault) {
- return internals.finalize(value, null, helpers);
- }
- state.mainstay.tracer.value(state, 'default', value, {});
- value = {};
- }
- }
- else if (presence === 'forbidden') {
- return internals.finalize(value, [schema.$_createError('any.unknown', value, null, state, prefs)], helpers);
- }
- // Allowed values
- const errors = [];
- if (schema._valids) {
- const match = schema._valids.get(value, state, prefs, schema._flags.insensitive);
- if (match) {
- if (prefs.convert) {
- state.mainstay.tracer.value(state, 'valids', value, match.value);
- value = match.value;
- }
- state.mainstay.tracer.filter(schema, state, 'valid', match);
- return internals.finalize(value, null, helpers);
- }
- if (schema._flags.only) {
- const report = schema.$_createError('any.only', value, { valids: schema._valids.values({ display: true }) }, state, prefs);
- if (prefs.abortEarly) {
- return internals.finalize(value, [report], helpers);
- }
- errors.push(report);
- }
- }
- // Denied values
- if (schema._invalids) {
- const match = schema._invalids.get(value, state, prefs, schema._flags.insensitive);
- if (match) {
- state.mainstay.tracer.filter(schema, state, 'invalid', match);
- const report = schema.$_createError('any.invalid', value, { invalids: schema._invalids.values({ display: true }) }, state, prefs);
- if (prefs.abortEarly) {
- return internals.finalize(value, [report], helpers);
- }
- errors.push(report);
- }
- }
- // Base type
- if (def.validate) {
- const base = def.validate(value, helpers);
- if (base) {
- state.mainstay.tracer.value(state, 'base', value, base.value);
- value = base.value;
- if (base.errors) {
- if (!Array.isArray(base.errors)) {
- errors.push(base.errors);
- return internals.finalize(value, errors, helpers); // Base error always aborts early
- }
- if (base.errors.length) {
- errors.push(...base.errors);
- return internals.finalize(value, errors, helpers); // Base error always aborts early
- }
- }
- }
- }
- // Validate tests
- if (!schema._rules.length) {
- return internals.finalize(value, errors, helpers);
- }
- return internals.rules(value, errors, helpers);
- };
- internals.rules = function (value, errors, helpers) {
- const { schema, state, prefs } = helpers;
- for (const rule of schema._rules) {
- const definition = schema._definition.rules[rule.method];
- // Skip rules that are also applied in coerce step
- if (definition.convert &&
- prefs.convert) {
- state.mainstay.tracer.log(schema, state, 'rule', rule.name, 'full');
- continue;
- }
- // Resolve references
- let ret;
- let args = rule.args;
- if (rule._resolve.length) {
- args = Object.assign({}, args); // Shallow copy
- for (const key of rule._resolve) {
- const resolver = definition.argsByName.get(key);
- const resolved = args[key].resolve(value, state, prefs);
- const normalized = resolver.normalize ? resolver.normalize(resolved) : resolved;
- const invalid = Common.validateArg(normalized, null, resolver);
- if (invalid) {
- ret = schema.$_createError('any.ref', resolved, { arg: key, ref: args[key], reason: invalid }, state, prefs);
- break;
- }
- args[key] = normalized;
- }
- }
- // Test rule
- ret = ret || definition.validate(value, helpers, args, rule); // Use ret if already set to reference error
- const result = internals.rule(ret, rule);
- if (result.errors) {
- state.mainstay.tracer.log(schema, state, 'rule', rule.name, 'error');
- if (rule.warn) {
- state.mainstay.warnings.push(...result.errors);
- continue;
- }
- if (prefs.abortEarly) {
- return internals.finalize(value, result.errors, helpers);
- }
- errors.push(...result.errors);
- }
- else {
- state.mainstay.tracer.log(schema, state, 'rule', rule.name, 'pass');
- state.mainstay.tracer.value(state, 'rule', value, result.value, rule.name);
- value = result.value;
- }
- }
- return internals.finalize(value, errors, helpers);
- };
- internals.rule = function (ret, rule) {
- if (ret instanceof Errors.Report) {
- internals.error(ret, rule);
- return { errors: [ret], value: null };
- }
- if (Array.isArray(ret) &&
- ret[Common.symbols.errors]) {
- ret.forEach((report) => internals.error(report, rule));
- return { errors: ret, value: null };
- }
- return { errors: null, value: ret };
- };
- internals.error = function (report, rule) {
- if (rule.message) {
- report._setTemplate(rule.message);
- }
- return report;
- };
- internals.finalize = function (value, errors, helpers) {
- errors = errors || [];
- const { schema, state, prefs } = helpers;
- // Failover value
- if (errors.length) {
- const failover = internals.default('failover', undefined, errors, helpers);
- if (failover !== undefined) {
- state.mainstay.tracer.value(state, 'failover', value, failover);
- value = failover;
- errors = [];
- }
- }
- // Error override
- if (errors.length &&
- schema._flags.error) {
- if (typeof schema._flags.error === 'function') {
- errors = schema._flags.error(errors);
- if (!Array.isArray(errors)) {
- errors = [errors];
- }
- for (const error of errors) {
- Assert(error instanceof Error || error instanceof Errors.Report, 'error() must return an Error object');
- }
- }
- else {
- errors = [schema._flags.error];
- }
- }
- // Default
- if (value === undefined) {
- const defaulted = internals.default('default', value, errors, helpers);
- state.mainstay.tracer.value(state, 'default', value, defaulted);
- value = defaulted;
- }
- // Cast
- if (schema._flags.cast &&
- value !== undefined) {
- const caster = schema._definition.cast[schema._flags.cast];
- if (caster.from(value)) {
- const casted = caster.to(value, helpers);
- state.mainstay.tracer.value(state, 'cast', value, casted, schema._flags.cast);
- value = casted;
- }
- }
- // Externals
- if (schema.$_terms.externals &&
- prefs.externals &&
- prefs._externals !== false) { // Disabled for matching
- for (const { method } of schema.$_terms.externals) {
- state.mainstay.externals.push({ method, schema, state, label: Errors.label(schema._flags, state, prefs) });
- }
- }
- // Result
- const result = { value, errors: errors.length ? errors : null };
- if (schema._flags.result) {
- result.value = schema._flags.result === 'strip' ? undefined : /* raw */ helpers.original;
- state.mainstay.tracer.value(state, schema._flags.result, value, result.value);
- state.shadow(value, schema._flags.result);
- }
- // Cache
- if (schema._cache &&
- prefs.cache !== false &&
- !schema._refs.length) {
- schema._cache.set(helpers.original, result);
- }
- // Artifacts
- if (value !== undefined &&
- !result.errors &&
- schema._flags.artifact !== undefined) {
- state.mainstay.artifacts = state.mainstay.artifacts || new Map();
- if (!state.mainstay.artifacts.has(schema._flags.artifact)) {
- state.mainstay.artifacts.set(schema._flags.artifact, []);
- }
- state.mainstay.artifacts.get(schema._flags.artifact).push(state.path);
- }
- return result;
- };
- internals.prefs = function (schema, prefs) {
- const isDefaultOptions = prefs === Common.defaults;
- if (isDefaultOptions &&
- schema._preferences[Common.symbols.prefs]) {
- return schema._preferences[Common.symbols.prefs];
- }
- prefs = Common.preferences(prefs, schema._preferences);
- if (isDefaultOptions) {
- schema._preferences[Common.symbols.prefs] = prefs;
- }
- return prefs;
- };
- internals.default = function (flag, value, errors, helpers) {
- const { schema, state, prefs } = helpers;
- const source = schema._flags[flag];
- if (prefs.noDefaults ||
- source === undefined) {
- return value;
- }
- state.mainstay.tracer.log(schema, state, 'rule', flag, 'full');
- if (!source) {
- return source;
- }
- if (typeof source === 'function') {
- const args = source.length ? [Clone(state.ancestors[0]), helpers] : [];
- try {
- return source(...args);
- }
- catch (err) {
- errors.push(schema.$_createError(`any.${flag}`, null, { error: err }, state, prefs));
- return;
- }
- }
- if (typeof source !== 'object') {
- return source;
- }
- if (source[Common.symbols.literal]) {
- return source.literal;
- }
- if (Common.isResolvable(source)) {
- return source.resolve(value, state, prefs);
- }
- return Clone(source);
- };
- internals.trim = function (value, schema) {
- if (typeof value !== 'string') {
- return value;
- }
- const trim = schema.$_getRule('trim');
- if (!trim ||
- !trim.args.enabled) {
- return value;
- }
- return value.trim();
- };
- internals.ignore = {
- active: false,
- debug: Ignore,
- entry: Ignore,
- filter: Ignore,
- log: Ignore,
- resolve: Ignore,
- value: Ignore
- };
- internals.errorsArray = function () {
- const errors = [];
- errors[Common.symbols.errors] = true;
- return errors;
- };
|