amis-rpc-design/node_modules/joi/lib/manifest.js
2023-10-07 19:42:30 +08:00

477 lines
12 KiB
JavaScript
Executable File

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