268 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			268 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| '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;
 | |
| };
 |