'use strict'; const Assert = require('@hapi/hoek/lib/assert'); const Merge = require('@hapi/hoek/lib/merge'); const Any = require('./any'); const Common = require('../common'); const Compile = require('../compile'); const Errors = require('../errors'); const Ref = require('../ref'); const internals = {}; module.exports = Any.extend({ type: 'alternatives', flags: { match: { default: 'any' } // 'any', 'one', 'all' }, terms: { matches: { init: [], register: Ref.toSibling } }, args(schema, ...schemas) { if (schemas.length === 1) { if (Array.isArray(schemas[0])) { return schema.try(...schemas[0]); } } return schema.try(...schemas); }, validate(value, helpers) { const { schema, error, state, prefs } = helpers; // Match all or one if (schema._flags.match) { const matched = []; for (let i = 0; i < schema.$_terms.matches.length; ++i) { const item = schema.$_terms.matches[i]; const localState = state.nest(item.schema, `match.${i}`); localState.snapshot(); const result = item.schema.$_validate(value, localState, prefs); if (!result.errors) { matched.push(result.value); } else { localState.restore(); } } if (matched.length === 0) { return { errors: error('alternatives.any') }; } if (schema._flags.match === 'one') { return matched.length === 1 ? { value: matched[0] } : { errors: error('alternatives.one') }; } if (matched.length !== schema.$_terms.matches.length) { return { errors: error('alternatives.all') }; } const allobj = schema.$_terms.matches.reduce((acc, v) => acc && v.schema.type === 'object', true); return allobj ? { value: matched.reduce((acc, v) => Merge(acc, v, { mergeArrays: false })) } : { value: matched[matched.length - 1] }; } // Match any const errors = []; for (let i = 0; i < schema.$_terms.matches.length; ++i) { const item = schema.$_terms.matches[i]; // Try if (item.schema) { const localState = state.nest(item.schema, `match.${i}`); localState.snapshot(); const result = item.schema.$_validate(value, localState, prefs); if (!result.errors) { return result; } localState.restore(); errors.push({ schema: item.schema, reports: result.errors }); continue; } // Conditional const input = item.ref ? item.ref.resolve(value, state, prefs) : value; const tests = item.is ? [item] : item.switch; for (let j = 0; j < tests.length; ++j) { const test = tests[j]; const { is, then, otherwise } = test; const id = `match.${i}${item.switch ? '.' + j : ''}`; if (!is.$_match(input, state.nest(is, `${id}.is`), prefs)) { if (otherwise) { return otherwise.$_validate(value, state.nest(otherwise, `${id}.otherwise`), prefs); } } else if (then) { return then.$_validate(value, state.nest(then, `${id}.then`), prefs); } } } return internals.errors(errors, helpers); }, rules: { conditional: { method(condition, options) { Assert(!this._flags._endedSwitch, 'Unreachable condition'); Assert(!this._flags.match, 'Cannot combine match mode', this._flags.match, 'with conditional rule'); Assert(options.break === undefined, 'Cannot use break option with alternatives conditional'); const obj = this.clone(); const match = Compile.when(obj, condition, options); const conditions = match.is ? [match] : match.switch; for (const item of conditions) { if (item.then && item.otherwise) { obj.$_setFlag('_endedSwitch', true, { clone: false }); break; } } obj.$_terms.matches.push(match); return obj.$_mutateRebuild(); } }, match: { method(mode) { Assert(['any', 'one', 'all'].includes(mode), 'Invalid alternatives match mode', mode); if (mode !== 'any') { for (const match of this.$_terms.matches) { Assert(match.schema, 'Cannot combine match mode', mode, 'with conditional rules'); } } return this.$_setFlag('match', mode); } }, try: { method(...schemas) { Assert(schemas.length, 'Missing alternative schemas'); Common.verifyFlat(schemas, 'try'); Assert(!this._flags._endedSwitch, 'Unreachable condition'); const obj = this.clone(); for (const schema of schemas) { obj.$_terms.matches.push({ schema: obj.$_compile(schema) }); } return obj.$_mutateRebuild(); } } }, overrides: { label(name) { const obj = this.$_parent('label', name); const each = (item, source) => (source.path[0] !== 'is' ? item.label(name) : undefined); return obj.$_modify({ each, ref: false }); } }, rebuild(schema) { // Flag when an alternative type is an array const each = (item) => { if (Common.isSchema(item) && item.type === 'array') { schema.$_setFlag('_arrayItems', true, { clone: false }); } }; schema.$_modify({ each }); }, manifest: { build(obj, desc) { if (desc.matches) { for (const match of desc.matches) { const { schema, ref, is, not, then, otherwise } = match; if (schema) { obj = obj.try(schema); } else if (ref) { obj = obj.conditional(ref, { is, then, not, otherwise, switch: match.switch }); } else { obj = obj.conditional(is, { then, otherwise }); } } } return obj; } }, messages: { 'alternatives.all': '{{#label}} does not match all of the required types', 'alternatives.any': '{{#label}} does not match any of the allowed types', 'alternatives.match': '{{#label}} does not match any of the allowed types', 'alternatives.one': '{{#label}} matches more than one allowed type', 'alternatives.types': '{{#label}} must be one of {{#types}}' } }); // Helpers internals.errors = function (failures, { error, state }) { // Nothing matched due to type criteria rules if (!failures.length) { return { errors: error('alternatives.any') }; } // Single error if (failures.length === 1) { return { errors: failures[0].reports }; } // Analyze reasons const valids = new Set(); const complex = []; for (const { reports, schema } of failures) { // Multiple errors (!abortEarly) if (reports.length > 1) { return internals.unmatched(failures, error); } // Custom error const report = reports[0]; if (report instanceof Errors.Report === false) { return internals.unmatched(failures, error); } // Internal object or array error if (report.state.path.length !== state.path.length) { complex.push({ type: schema.type, report }); continue; } // Valids if (report.code === 'any.only') { for (const valid of report.local.valids) { valids.add(valid); } continue; } // Base type const [type, code] = report.code.split('.'); if (code !== 'base') { complex.push({ type: schema.type, report }); continue; } valids.add(type); } // All errors are base types or valids if (!complex.length) { return { errors: error('alternatives.types', { types: [...valids] }) }; } // Single complex error if (complex.length === 1) { return { errors: complex[0].report }; } return internals.unmatched(failures, error); }; internals.unmatched = function (failures, error) { const errors = []; for (const failure of failures) { errors.push(...failure.reports); } return { errors: error('alternatives.match', Errors.details(errors, { override: false })) }; };