You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
653 lines
18 KiB
653 lines
18 KiB
'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; |
|
for (const { method, path, label } of mainstay.externals) { |
|
let node = root; |
|
let key; |
|
let parent; |
|
|
|
if (path.length) { |
|
key = path[path.length - 1]; |
|
parent = Reach(root, path.slice(0, -1)); |
|
node = parent[key]; |
|
} |
|
|
|
try { |
|
const output = await method(node, { prefs }); |
|
if (output === undefined || |
|
output === node) { |
|
|
|
continue; |
|
} |
|
|
|
if (parent) { |
|
parent[key] = output; |
|
} |
|
else { |
|
root = output; |
|
} |
|
} |
|
catch (err) { |
|
if (settings.errors.label) { |
|
err.message += ` (${label})`; // Change message to include path |
|
} |
|
|
|
throw err; |
|
} |
|
} |
|
|
|
result.value = root; |
|
} |
|
|
|
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.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 = { externals: [], warnings: [], 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, path: state.path, 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; |
|
};
|
|
|