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.
267 lines
6.7 KiB
267 lines
6.7 KiB
'use strict'; |
|
|
|
const Assert = require('@hapi/hoek/lib/assert'); |
|
|
|
const Common = require('./common'); |
|
const Ref = require('./ref'); |
|
|
|
|
|
const internals = {}; |
|
|
|
|
|
|
|
exports.Ids = internals.Ids = class { |
|
|
|
constructor() { |
|
|
|
this._byId = new Map(); |
|
this._byKey = new Map(); |
|
this._schemaChain = false; |
|
} |
|
|
|
clone() { |
|
|
|
const clone = new internals.Ids(); |
|
clone._byId = new Map(this._byId); |
|
clone._byKey = new Map(this._byKey); |
|
clone._schemaChain = this._schemaChain; |
|
return clone; |
|
} |
|
|
|
concat(source) { |
|
|
|
if (source._schemaChain) { |
|
this._schemaChain = true; |
|
} |
|
|
|
for (const [id, value] of source._byId.entries()) { |
|
Assert(!this._byKey.has(id), 'Schema id conflicts with existing key:', id); |
|
this._byId.set(id, value); |
|
} |
|
|
|
for (const [key, value] of source._byKey.entries()) { |
|
Assert(!this._byId.has(key), 'Schema key conflicts with existing id:', key); |
|
this._byKey.set(key, value); |
|
} |
|
} |
|
|
|
fork(path, adjuster, root) { |
|
|
|
const chain = this._collect(path); |
|
chain.push({ schema: root }); |
|
const tail = chain.shift(); |
|
let adjusted = { id: tail.id, schema: adjuster(tail.schema) }; |
|
|
|
Assert(Common.isSchema(adjusted.schema), 'adjuster function failed to return a joi schema type'); |
|
|
|
for (const node of chain) { |
|
adjusted = { id: node.id, schema: internals.fork(node.schema, adjusted.id, adjusted.schema) }; |
|
} |
|
|
|
return adjusted.schema; |
|
} |
|
|
|
labels(path, behind = []) { |
|
|
|
const current = path[0]; |
|
const node = this._get(current); |
|
if (!node) { |
|
return [...behind, ...path].join('.'); |
|
} |
|
|
|
const forward = path.slice(1); |
|
behind = [...behind, node.schema._flags.label || current]; |
|
if (!forward.length) { |
|
return behind.join('.'); |
|
} |
|
|
|
return node.schema._ids.labels(forward, behind); |
|
} |
|
|
|
reach(path, behind = []) { |
|
|
|
const current = path[0]; |
|
const node = this._get(current); |
|
Assert(node, 'Schema does not contain path', [...behind, ...path].join('.')); |
|
|
|
const forward = path.slice(1); |
|
if (!forward.length) { |
|
return node.schema; |
|
} |
|
|
|
return node.schema._ids.reach(forward, [...behind, current]); |
|
} |
|
|
|
register(schema, { key } = {}) { |
|
|
|
if (!schema || |
|
!Common.isSchema(schema)) { |
|
|
|
return; |
|
} |
|
|
|
if (schema.$_property('schemaChain') || |
|
schema._ids._schemaChain) { |
|
|
|
this._schemaChain = true; |
|
} |
|
|
|
const id = schema._flags.id; |
|
if (id) { |
|
const existing = this._byId.get(id); |
|
Assert(!existing || existing.schema === schema, 'Cannot add different schemas with the same id:', id); |
|
Assert(!this._byKey.has(id), 'Schema id conflicts with existing key:', id); |
|
|
|
this._byId.set(id, { schema, id }); |
|
} |
|
|
|
if (key) { |
|
Assert(!this._byKey.has(key), 'Schema already contains key:', key); |
|
Assert(!this._byId.has(key), 'Schema key conflicts with existing id:', key); |
|
|
|
this._byKey.set(key, { schema, id: key }); |
|
} |
|
} |
|
|
|
reset() { |
|
|
|
this._byId = new Map(); |
|
this._byKey = new Map(); |
|
this._schemaChain = false; |
|
} |
|
|
|
_collect(path, behind = [], nodes = []) { |
|
|
|
const current = path[0]; |
|
const node = this._get(current); |
|
Assert(node, 'Schema does not contain path', [...behind, ...path].join('.')); |
|
|
|
nodes = [node, ...nodes]; |
|
|
|
const forward = path.slice(1); |
|
if (!forward.length) { |
|
return nodes; |
|
} |
|
|
|
return node.schema._ids._collect(forward, [...behind, current], nodes); |
|
} |
|
|
|
_get(id) { |
|
|
|
return this._byId.get(id) || this._byKey.get(id); |
|
} |
|
}; |
|
|
|
|
|
internals.fork = function (schema, id, replacement) { |
|
|
|
const each = (item, { key }) => { |
|
|
|
if (id === (item._flags.id || key)) { |
|
return replacement; |
|
} |
|
}; |
|
|
|
const obj = exports.schema(schema, { each, ref: false }); |
|
return obj ? obj.$_mutateRebuild() : schema; |
|
}; |
|
|
|
|
|
exports.schema = function (schema, options) { |
|
|
|
let obj; |
|
|
|
for (const name in schema._flags) { |
|
if (name[0] === '_') { |
|
continue; |
|
} |
|
|
|
const result = internals.scan(schema._flags[name], { source: 'flags', name }, options); |
|
if (result !== undefined) { |
|
obj = obj || schema.clone(); |
|
obj._flags[name] = result; |
|
} |
|
} |
|
|
|
for (let i = 0; i < schema._rules.length; ++i) { |
|
const rule = schema._rules[i]; |
|
const result = internals.scan(rule.args, { source: 'rules', name: rule.name }, options); |
|
if (result !== undefined) { |
|
obj = obj || schema.clone(); |
|
const clone = Object.assign({}, rule); |
|
clone.args = result; |
|
obj._rules[i] = clone; |
|
|
|
const existingUnique = obj._singleRules.get(rule.name); |
|
if (existingUnique === rule) { |
|
obj._singleRules.set(rule.name, clone); |
|
} |
|
} |
|
} |
|
|
|
for (const name in schema.$_terms) { |
|
if (name[0] === '_') { |
|
continue; |
|
} |
|
|
|
const result = internals.scan(schema.$_terms[name], { source: 'terms', name }, options); |
|
if (result !== undefined) { |
|
obj = obj || schema.clone(); |
|
obj.$_terms[name] = result; |
|
} |
|
} |
|
|
|
return obj; |
|
}; |
|
|
|
|
|
internals.scan = function (item, source, options, _path, _key) { |
|
|
|
const path = _path || []; |
|
|
|
if (item === null || |
|
typeof item !== 'object') { |
|
|
|
return; |
|
} |
|
|
|
let clone; |
|
|
|
if (Array.isArray(item)) { |
|
for (let i = 0; i < item.length; ++i) { |
|
const key = source.source === 'terms' && source.name === 'keys' && item[i].key; |
|
const result = internals.scan(item[i], source, options, [i, ...path], key); |
|
if (result !== undefined) { |
|
clone = clone || item.slice(); |
|
clone[i] = result; |
|
} |
|
} |
|
|
|
return clone; |
|
} |
|
|
|
if (options.schema !== false && Common.isSchema(item) || |
|
options.ref !== false && Ref.isRef(item)) { |
|
|
|
const result = options.each(item, { ...source, path, key: _key }); |
|
if (result === item) { |
|
return; |
|
} |
|
|
|
return result; |
|
} |
|
|
|
for (const key in item) { |
|
if (key[0] === '_') { |
|
continue; |
|
} |
|
|
|
const result = internals.scan(item[key], source, options, [key, ...path], _key); |
|
if (result !== undefined) { |
|
clone = clone || Object.assign({}, item); |
|
clone[key] = result; |
|
} |
|
} |
|
|
|
return clone; |
|
};
|
|
|