|
|
'use strict';
const Assert = require('@hapi/hoek/lib/assert'); const Clone = require('@hapi/hoek/lib/clone');
const Common = require('./common'); const Messages = require('./messages'); const Ref = require('./ref'); const Template = require('./template');
let Schemas;
const internals = {};
exports.describe = function (schema) {
const def = schema._definition;
// Type
const desc = { type: schema.type, flags: {}, rules: [] };
// Flags
for (const flag in schema._flags) { if (flag[0] !== '_') { desc.flags[flag] = internals.describe(schema._flags[flag]); } }
if (!Object.keys(desc.flags).length) { delete desc.flags; }
// Preferences
if (schema._preferences) { desc.preferences = Clone(schema._preferences, { shallow: ['messages'] }); delete desc.preferences[Common.symbols.prefs]; if (desc.preferences.messages) { desc.preferences.messages = Messages.decompile(desc.preferences.messages); } }
// Allow / Invalid
if (schema._valids) { desc.allow = schema._valids.describe(); }
if (schema._invalids) { desc.invalid = schema._invalids.describe(); }
// Rules
for (const rule of schema._rules) { const ruleDef = def.rules[rule.name]; if (ruleDef.manifest === false) { // Defaults to true
continue; }
const item = { name: rule.name };
for (const custom in def.modifiers) { if (rule[custom] !== undefined) { item[custom] = internals.describe(rule[custom]); } }
if (rule.args) { item.args = {}; for (const key in rule.args) { const arg = rule.args[key]; if (key === 'options' && !Object.keys(arg).length) {
continue; }
item.args[key] = internals.describe(arg, { assign: key }); }
if (!Object.keys(item.args).length) { delete item.args; } }
desc.rules.push(item); }
if (!desc.rules.length) { delete desc.rules; }
// Terms (must be last to verify no name conflicts)
for (const term in schema.$_terms) { if (term[0] === '_') { continue; }
Assert(!desc[term], 'Cannot describe schema due to internal name conflict with', term);
const items = schema.$_terms[term]; if (!items) { continue; }
if (items instanceof Map) { if (items.size) { desc[term] = [...items.entries()]; }
continue; }
if (Common.isValues(items)) { desc[term] = items.describe(); continue; }
Assert(def.terms[term], 'Term', term, 'missing configuration'); const manifest = def.terms[term].manifest; const mapped = typeof manifest === 'object'; if (!items.length && !mapped) {
continue; }
const normalized = []; for (const item of items) { normalized.push(internals.describe(item)); }
// Mapped
if (mapped) { const { from, to } = manifest.mapped; desc[term] = {}; for (const item of normalized) { desc[term][item[to]] = item[from]; }
continue; }
// Single
if (manifest === 'single') { Assert(normalized.length === 1, 'Term', term, 'contains more than one item'); desc[term] = normalized[0]; continue; }
// Array
desc[term] = normalized; }
internals.validate(schema.$_root, desc); return desc; };
internals.describe = function (item, options = {}) {
if (Array.isArray(item)) { return item.map(internals.describe); }
if (item === Common.symbols.deepDefault) { return { special: 'deep' }; }
if (typeof item !== 'object' || item === null) {
return item; }
if (options.assign === 'options') { return Clone(item); }
if (Buffer && Buffer.isBuffer(item)) { // $lab:coverage:ignore$
return { buffer: item.toString('binary') }; }
if (item instanceof Date) { return item.toISOString(); }
if (item instanceof Error) { return item; }
if (item instanceof RegExp) { if (options.assign === 'regex') { return item.toString(); }
return { regex: item.toString() }; }
if (item[Common.symbols.literal]) { return { function: item.literal }; }
if (typeof item.describe === 'function') { if (options.assign === 'ref') { return item.describe().ref; }
return item.describe(); }
const normalized = {}; for (const key in item) { const value = item[key]; if (value === undefined) { continue; }
normalized[key] = internals.describe(value, { assign: key }); }
return normalized; };
exports.build = function (joi, desc) {
const builder = new internals.Builder(joi); return builder.parse(desc); };
internals.Builder = class {
constructor(joi) {
this.joi = joi; }
parse(desc) {
internals.validate(this.joi, desc);
// Type
let schema = this.joi[desc.type]()._bare(); const def = schema._definition;
// Flags
if (desc.flags) { for (const flag in desc.flags) { const setter = def.flags[flag] && def.flags[flag].setter || flag; Assert(typeof schema[setter] === 'function', 'Invalid flag', flag, 'for type', desc.type); schema = schema[setter](this.build(desc.flags[flag])); } }
// Preferences
if (desc.preferences) { schema = schema.preferences(this.build(desc.preferences)); }
// Allow / Invalid
if (desc.allow) { schema = schema.allow(...this.build(desc.allow)); }
if (desc.invalid) { schema = schema.invalid(...this.build(desc.invalid)); }
// Rules
if (desc.rules) { for (const rule of desc.rules) { Assert(typeof schema[rule.name] === 'function', 'Invalid rule', rule.name, 'for type', desc.type);
const args = []; if (rule.args) { const built = {}; for (const key in rule.args) { built[key] = this.build(rule.args[key], { assign: key }); }
const keys = Object.keys(built); const definition = def.rules[rule.name].args; if (definition) { Assert(keys.length <= definition.length, 'Invalid number of arguments for', desc.type, rule.name, '(expected up to', definition.length, ', found', keys.length, ')'); for (const { name } of definition) { args.push(built[name]); } } else { Assert(keys.length === 1, 'Invalid number of arguments for', desc.type, rule.name, '(expected up to 1, found', keys.length, ')'); args.push(built[keys[0]]); } }
// Apply
schema = schema[rule.name](...args);
// Ruleset
const options = {}; for (const custom in def.modifiers) { if (rule[custom] !== undefined) { options[custom] = this.build(rule[custom]); } }
if (Object.keys(options).length) { schema = schema.rule(options); } } }
// Terms
const terms = {}; for (const key in desc) { if (['allow', 'flags', 'invalid', 'whens', 'preferences', 'rules', 'type'].includes(key)) { continue; }
Assert(def.terms[key], 'Term', key, 'missing configuration'); const manifest = def.terms[key].manifest;
if (manifest === 'schema') { terms[key] = desc[key].map((item) => this.parse(item)); continue; }
if (manifest === 'values') { terms[key] = desc[key].map((item) => this.build(item)); continue; }
if (manifest === 'single') { terms[key] = this.build(desc[key]); continue; }
if (typeof manifest === 'object') { terms[key] = {}; for (const name in desc[key]) { const value = desc[key][name]; terms[key][name] = this.parse(value); }
continue; }
terms[key] = this.build(desc[key]); }
if (desc.whens) { terms.whens = desc.whens.map((when) => this.build(when)); }
schema = def.manifest.build(schema, terms); schema.$_temp.ruleset = false; return schema; }
build(desc, options = {}) {
if (desc === null) { return null; }
if (Array.isArray(desc)) { return desc.map((item) => this.build(item)); }
if (desc instanceof Error) { return desc; }
if (options.assign === 'options') { return Clone(desc); }
if (options.assign === 'regex') { return internals.regex(desc); }
if (options.assign === 'ref') { return Ref.build(desc); }
if (typeof desc !== 'object') { return desc; }
if (Object.keys(desc).length === 1) { if (desc.buffer) { Assert(Buffer, 'Buffers are not supported'); return Buffer && Buffer.from(desc.buffer, 'binary'); // $lab:coverage:ignore$
}
if (desc.function) { return { [Common.symbols.literal]: true, literal: desc.function }; }
if (desc.override) { return Common.symbols.override; }
if (desc.ref) { return Ref.build(desc.ref); }
if (desc.regex) { return internals.regex(desc.regex); }
if (desc.special) { Assert(['deep'].includes(desc.special), 'Unknown special value', desc.special); return Common.symbols.deepDefault; }
if (desc.value) { return Clone(desc.value); } }
if (desc.type) { return this.parse(desc); }
if (desc.template) { return Template.build(desc); }
const normalized = {}; for (const key in desc) { normalized[key] = this.build(desc[key], { assign: key }); }
return normalized; } };
internals.regex = function (string) {
const end = string.lastIndexOf('/'); const exp = string.slice(1, end); const flags = string.slice(end + 1); return new RegExp(exp, flags); };
internals.validate = function (joi, desc) {
Schemas = Schemas || require('./schemas');
joi.assert(desc, Schemas.description); };
|