123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476 |
- '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);
- };
|