'use strict'; const Assert = require('@hapi/hoek/lib/assert'); const Any = require('./any'); const Common = require('../common'); const Compile = require('../compile'); const Errors = require('../errors'); const internals = {}; module.exports = Any.extend({ type: 'link', properties: { schemaChain: true }, terms: { link: { init: null, manifest: 'single', register: false } }, args(schema, ref) { return schema.ref(ref); }, validate(value, { schema, state, prefs }) { Assert(schema.$_terms.link, 'Uninitialized link schema'); const linked = internals.generate(schema, value, state, prefs); const ref = schema.$_terms.link[0].ref; return linked.$_validate(value, state.nest(linked, `link:${ref.display}:${linked.type}`), prefs); }, generate(schema, value, state, prefs) { return internals.generate(schema, value, state, prefs); }, rules: { ref: { method(ref) { Assert(!this.$_terms.link, 'Cannot reinitialize schema'); ref = Compile.ref(ref); Assert(ref.type === 'value' || ref.type === 'local', 'Invalid reference type:', ref.type); Assert(ref.type === 'local' || ref.ancestor === 'root' || ref.ancestor > 0, 'Link cannot reference itself'); const obj = this.clone(); obj.$_terms.link = [{ ref }]; return obj; } }, relative: { method(enabled = true) { return this.$_setFlag('relative', enabled); } } }, overrides: { concat(source) { Assert(this.$_terms.link, 'Uninitialized link schema'); Assert(Common.isSchema(source), 'Invalid schema object'); Assert(source.type !== 'link', 'Cannot merge type link with another link'); const obj = this.clone(); if (!obj.$_terms.whens) { obj.$_terms.whens = []; } obj.$_terms.whens.push({ concat: source }); return obj.$_mutateRebuild(); } }, manifest: { build(obj, desc) { Assert(desc.link, 'Invalid link description missing link'); return obj.ref(desc.link); } } }); // Helpers internals.generate = function (schema, value, state, prefs) { let linked = state.mainstay.links.get(schema); if (linked) { return linked._generate(value, state, prefs).schema; } const ref = schema.$_terms.link[0].ref; const { perspective, path } = internals.perspective(ref, state); internals.assert(perspective, 'which is outside of schema boundaries', ref, schema, state, prefs); try { linked = path.length ? perspective.$_reach(path) : perspective; } catch (ignoreErr) { internals.assert(false, 'to non-existing schema', ref, schema, state, prefs); } internals.assert(linked.type !== 'link', 'which is another link', ref, schema, state, prefs); if (!schema._flags.relative) { state.mainstay.links.set(schema, linked); } return linked._generate(value, state, prefs).schema; }; internals.perspective = function (ref, state) { if (ref.type === 'local') { for (const { schema, key } of state.schemas) { // From parent to root const id = schema._flags.id || key; if (id === ref.path[0]) { return { perspective: schema, path: ref.path.slice(1) }; } if (schema.$_terms.shared) { for (const shared of schema.$_terms.shared) { if (shared._flags.id === ref.path[0]) { return { perspective: shared, path: ref.path.slice(1) }; } } } } return { perspective: null, path: null }; } if (ref.ancestor === 'root') { return { perspective: state.schemas[state.schemas.length - 1].schema, path: ref.path }; } return { perspective: state.schemas[ref.ancestor] && state.schemas[ref.ancestor].schema, path: ref.path }; }; internals.assert = function (condition, message, ref, schema, state, prefs) { if (condition) { // Manual check to avoid generating error message on success return; } Assert(false, `"${Errors.label(schema._flags, state, prefs)}" contains link reference "${ref.display}" ${message}`); };