'use strict'; const Assert = require('@hapi/hoek/lib/assert'); const Clone = require('@hapi/hoek/lib/clone'); const Reach = require('@hapi/hoek/lib/reach'); const Common = require('./common'); let Template; const internals = { symbol: Symbol('ref'), // Used to internally identify references (shared with other joi versions) defaults: { adjust: null, in: false, iterables: null, map: null, separator: '.', type: 'value' } }; exports.create = function (key, options = {}) { Assert(typeof key === 'string', 'Invalid reference key:', key); Common.assertOptions(options, ['adjust', 'ancestor', 'in', 'iterables', 'map', 'prefix', 'render', 'separator']); Assert(!options.prefix || typeof options.prefix === 'object', 'options.prefix must be of type object'); const ref = Object.assign({}, internals.defaults, options); delete ref.prefix; const separator = ref.separator; const context = internals.context(key, separator, options.prefix); ref.type = context.type; key = context.key; if (ref.type === 'value') { if (context.root) { Assert(!separator || key[0] !== separator, 'Cannot specify relative path with root prefix'); ref.ancestor = 'root'; if (!key) { key = null; } } if (separator && separator === key) { key = null; ref.ancestor = 0; } else { if (ref.ancestor !== undefined) { Assert(!separator || !key || key[0] !== separator, 'Cannot combine prefix with ancestor option'); } else { const [ancestor, slice] = internals.ancestor(key, separator); if (slice) { key = key.slice(slice); if (key === '') { key = null; } } ref.ancestor = ancestor; } } } ref.path = separator ? (key === null ? [] : key.split(separator)) : [key]; return new internals.Ref(ref); }; exports.in = function (key, options = {}) { return exports.create(key, { ...options, in: true }); }; exports.isRef = function (ref) { return ref ? !!ref[Common.symbols.ref] : false; }; internals.Ref = class { constructor(options) { Assert(typeof options === 'object', 'Invalid reference construction'); Common.assertOptions(options, [ 'adjust', 'ancestor', 'in', 'iterables', 'map', 'path', 'render', 'separator', 'type', // Copied 'depth', 'key', 'root', 'display' // Overridden ]); Assert([false, undefined].includes(options.separator) || typeof options.separator === 'string' && options.separator.length === 1, 'Invalid separator'); Assert(!options.adjust || typeof options.adjust === 'function', 'options.adjust must be a function'); Assert(!options.map || Array.isArray(options.map), 'options.map must be an array'); Assert(!options.map || !options.adjust, 'Cannot set both map and adjust options'); Object.assign(this, internals.defaults, options); Assert(this.type === 'value' || this.ancestor === undefined, 'Non-value references cannot reference ancestors'); if (Array.isArray(this.map)) { this.map = new Map(this.map); } this.depth = this.path.length; this.key = this.path.length ? this.path.join(this.separator) : null; this.root = this.path[0]; this.updateDisplay(); } resolve(value, state, prefs, local, options = {}) { Assert(!this.in || options.in, 'Invalid in() reference usage'); if (this.type === 'global') { return this._resolve(prefs.context, state, options); } if (this.type === 'local') { return this._resolve(local, state, options); } if (!this.ancestor) { return this._resolve(value, state, options); } if (this.ancestor === 'root') { return this._resolve(state.ancestors[state.ancestors.length - 1], state, options); } Assert(this.ancestor <= state.ancestors.length, 'Invalid reference exceeds the schema root:', this.display); return this._resolve(state.ancestors[this.ancestor - 1], state, options); } _resolve(target, state, options) { let resolved; if (this.type === 'value' && state.mainstay.shadow && options.shadow !== false) { resolved = state.mainstay.shadow.get(this.absolute(state)); } if (resolved === undefined) { resolved = Reach(target, this.path, { iterables: this.iterables, functions: true }); } if (this.adjust) { resolved = this.adjust(resolved); } if (this.map) { const mapped = this.map.get(resolved); if (mapped !== undefined) { resolved = mapped; } } if (state.mainstay) { state.mainstay.tracer.resolve(state, this, resolved); } return resolved; } toString() { return this.display; } absolute(state) { return [...state.path.slice(0, -this.ancestor), ...this.path]; } clone() { return new internals.Ref(this); } describe() { const ref = { path: this.path }; if (this.type !== 'value') { ref.type = this.type; } if (this.separator !== '.') { ref.separator = this.separator; } if (this.type === 'value' && this.ancestor !== 1) { ref.ancestor = this.ancestor; } if (this.map) { ref.map = [...this.map]; } for (const key of ['adjust', 'iterables', 'render']) { if (this[key] !== null && this[key] !== undefined) { ref[key] = this[key]; } } if (this.in !== false) { ref.in = true; } return { ref }; } updateDisplay() { const key = this.key !== null ? this.key : ''; if (this.type !== 'value') { this.display = `ref:${this.type}:${key}`; return; } if (!this.separator) { this.display = `ref:${key}`; return; } if (!this.ancestor) { this.display = `ref:${this.separator}${key}`; return; } if (this.ancestor === 'root') { this.display = `ref:root:${key}`; return; } if (this.ancestor === 1) { this.display = `ref:${key || '..'}`; return; } const lead = new Array(this.ancestor + 1).fill(this.separator).join(''); this.display = `ref:${lead}${key || ''}`; } }; internals.Ref.prototype[Common.symbols.ref] = true; exports.build = function (desc) { desc = Object.assign({}, internals.defaults, desc); if (desc.type === 'value' && desc.ancestor === undefined) { desc.ancestor = 1; } return new internals.Ref(desc); }; internals.context = function (key, separator, prefix = {}) { key = key.trim(); if (prefix) { const globalp = prefix.global === undefined ? '$' : prefix.global; if (globalp !== separator && key.startsWith(globalp)) { return { key: key.slice(globalp.length), type: 'global' }; } const local = prefix.local === undefined ? '#' : prefix.local; if (local !== separator && key.startsWith(local)) { return { key: key.slice(local.length), type: 'local' }; } const root = prefix.root === undefined ? '/' : prefix.root; if (root !== separator && key.startsWith(root)) { return { key: key.slice(root.length), type: 'value', root: true }; } } return { key, type: 'value' }; }; internals.ancestor = function (key, separator) { if (!separator) { return [1, 0]; // 'a_b' -> 1 (parent) } if (key[0] !== separator) { // 'a.b' -> 1 (parent) return [1, 0]; } if (key[1] !== separator) { // '.a.b' -> 0 (self) return [0, 1]; } let i = 2; while (key[i] === separator) { ++i; } return [i - 1, i]; // '...a.b.' -> 2 (grandparent) }; exports.toSibling = 0; exports.toParent = 1; exports.Manager = class { constructor() { this.refs = []; // 0: [self refs], 1: [parent refs], 2: [grandparent refs], ... } register(source, target) { if (!source) { return; } target = target === undefined ? exports.toParent : target; // Array if (Array.isArray(source)) { for (const ref of source) { this.register(ref, target); } return; } // Schema if (Common.isSchema(source)) { for (const item of source._refs.refs) { if (item.ancestor - target >= 0) { this.refs.push({ ancestor: item.ancestor - target, root: item.root }); } } return; } // Reference if (exports.isRef(source) && source.type === 'value' && source.ancestor - target >= 0) { this.refs.push({ ancestor: source.ancestor - target, root: source.root }); } // Template Template = Template || require('./template'); if (Template.isTemplate(source)) { this.register(source.refs(), target); } } get length() { return this.refs.length; } clone() { const copy = new exports.Manager(); copy.refs = Clone(this.refs); return copy; } reset() { this.refs = []; } roots() { return this.refs.filter((ref) => !ref.ancestor).map((ref) => ref.root); } };