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.
476 lines
12 KiB
476 lines
12 KiB
'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); |
|
};
|
|
|