UNPKG

joi

Version:

Object schema validation

225 lines (156 loc) 6.1 kB
'use strict'; const { assert } = require('@hapi/hoek'); 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); }, jsonSchema(schema, res, mode, options) { if (!schema.$_terms.link) { return res; } const { ref } = schema.$_terms.link[0]; if (ref.ancestor === 'root' || ref.ancestor > 0) { res.$ref = `#/${ref.path.map((p) => `properties/${p}`).join('/')}`; return res; } if (ref.path.length === 1) { res.$ref = `#/$defs/${ref.path[0]}`; } else { res.$ref = `#/${ref.path.slice(1).map((p) => `properties/${p}`).join('/')}`; } return res; }, validate(value, { schema, state, prefs, error }) { assert(schema.$_terms.link, 'Uninitialized link schema'); const limit = schema._flags.maxRecursion; if (limit !== undefined && state.schemas.filter((entry) => entry.schema === schema).length > limit) { return { value, errors: error('link.maxRecursion', { limit }) }; } const linked = internals.generate(schema, value, state, prefs); const ref = schema.$_terms.link[0].ref; try { return linked.$_validate(value, state.nest(linked, `link:${ref.display}:${linked.type}`), prefs); } catch (err) { /* $lab:coverage:off$ */ if (!(err instanceof RangeError)) { throw err; } /* $lab:coverage:on$ */ return { value, errors: error('link.depth') }; } }, 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); } }, maxRecursion: { method(limit) { assert(Number.isSafeInteger(limit) && limit >= 1, 'limit must be a positive integer'); return this.$_setFlag('maxRecursion', limit); } } }, messages: { 'link.depth': '{{#label}} exceeds maximum recursion depth supported by the runtime', 'link.maxRecursion': '{{#label}} exceeds maximum recursion depth of {{#limit}}' }, 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 { 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}`); };