Sindbad~EG File Manager
'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 = [];
const failed = [];
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);
localState.commit();
}
else {
failed.push(result.errors);
localState.restore();
}
}
if (matched.length === 0) {
const context = {
details: failed.map((f) => Errors.details(f, { override: false }))
};
return { errors: error('alternatives.any', context) };
}
// Match one
if (schema._flags.match === 'one') {
return matched.length === 1 ? { value: matched[0] } : { errors: error('alternatives.one') };
}
// Match all
if (matched.length !== schema.$_terms.matches.length) {
const context = {
details: failed.map((f) => Errors.details(f, { override: false }))
};
return { errors: error('alternatives.all', context) };
}
const isAnyObj = (alternative) => {
return alternative.$_terms.matches.some((v) => {
return v.schema.type === 'object' ||
(v.schema.type === 'alternatives' && isAnyObj(v.schema));
});
};
return isAnyObj(schema) ? { 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) {
localState.commit();
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) => {
return source.path[0] !== 'is' && typeof item._flags.label !== 'string' ? 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 })) };
};
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists