code:
'use strict';

const Assert = require('@hapi/hoek/lib/assert');
const Clone = require('@hapi/hoek/lib/clone');

const Common = require('./common');
const Messages = require('./messages');
const Ref = require('./ref');
const Template = require('./template');

let Schemas;

const internals = {};

exports.describe = function (schema) {

    const def = schema._definition;

    // Type

    const desc = {
        type: schema.type,
        flags: {},
        rules: []
    };

    // Flags

    for (const flag in schema._flags) {
        if (flag[0] !== '_') {
            desc.flags[flag] = internals.describe(schema._flags[flag]);
        }
    }

    if (!Object.keys(desc.flags).length) {
        delete desc.flags;
    }

    // Preferences

    if (schema._preferences) {
        desc.preferences = Clone(schema._preferences, { shallow: ['messages'] });
        delete desc.preferences[Common.symbols.prefs];
        if (desc.preferences.messages) {
            desc.preferences.messages = Messages.decompile(desc.preferences.messages);
        }
    }

    // Allow / Invalid

    if (schema._valids) {
        desc.allow = schema._valids.describe();
    }

    if (schema._invalids) {
        desc.invalid = schema._invalids.describe();
    }

    // Rules

    for (const rule of schema._rules) {
        const ruleDef = def.rules[rule.name];
        if (ruleDef.manifest === false) {                           // Defaults to true
            continue;
        }

        const item = { name: rule.name };

        for (const custom in def.modifiers) {
            if (rule[custom] !== undefined) {
                item[custom] = internals.describe(rule[custom]);
            }
        }

        if (rule.args) {
            item.args = {};
            for (const key in rule.args) {
                const arg = rule.args[key];
                if (key === 'options' &&
                    !Object.keys(arg).length) {

                    continue;
                }

                item.args[key] = internals.describe(arg, { assign: key });
            }

            if (!Object.keys(item.args).length) {
                delete item.args;
            }
        }

        desc.rules.push(item);
    }

    if (!desc.rules.length) {
        delete desc.rules;
    }

    // Terms (must be last to verify no name conflicts)

    for (const term in schema.$_terms) {
        if (term[0] === '_') {
            continue;
        }

        Assert(!desc[term], 'Cannot describe schema due to internal name conflict with', term);

        const items = schema.$_terms[term];
        if (!items) {
            continue;
        }

        if (items instanceof Map) {
            if (items.size) {
                desc[term] = [...items.entries()];
            }

            continue;
        }

        if (Common.isValues(items)) {
            desc[term] = items.describe();
            continue;
        }

        Assert(def.terms[term], 'Term', term, 'missing configuration');
        const manifest = def.terms[term].manifest;
        const mapped = typeof manifest === 'object';
        if (!items.length &&
            !mapped) {

            continue;
        }

        const normalized = [];
        for (const item of items) {
            normalized.push(internals.describe(item));
        }

        // Mapped

        if (mapped) {
            const { from, to } = manifest.mapped;
            desc[term] = {};
            for (const item of normalized) {
                desc[term][item[to]] = item[from];
            }

            continue;
        }

        // Single

        if (manifest === 'single') {
            Assert(normalized.length === 1, 'Term', term, 'contains more than one item');
            desc[term] = normalized[0];
            continue;
        }

        // Array

        desc[term] = normalized;
    }

    internals.validate(schema.$_root, desc);
    return desc;
};

internals.describe = function (item, options = {}) {

    if (Array.isArray(item)) {
        return item.map(internals.describe);
    }

    if (item === Common.symbols.deepDefault) {
        return { special: 'deep' };
    }

    if (typeof item !== 'object' ||
        item === null) {

        return item;
    }

    if (options.assign === 'options') {
        return Clone(item);
    }

    if (Buffer && Buffer.isBuffer(item)) {                          // $lab:coverage:ignore$
        return { buffer: item.toString('binary') };
    }

    if (item instanceof Date) {
        return item.toISOString();
    }

    if (item instanceof Error) {
        return item;
    }

    if (item instanceof RegExp) {
        if (options.assign === 'regex') {
            return item.toString();
        }

        return { regex: item.toString() };
    }

    if (item[Common.symbols.literal]) {
        return { function: item.literal };
    }

    if (typeof item.describe === 'function') {
        if (options.assign === 'ref') {
            return item.describe().ref;
        }

        return item.describe();
    }

    const normalized = {};
    for (const key in item) {
        const value = item[key];
        if (value === undefined) {
            continue;
        }

        normalized[key] = internals.describe(value, { assign: key });
    }

    return normalized;
};

exports.build = function (joi, desc) {

    const builder = new internals.Builder(joi);
    return builder.parse(desc);
};

internals.Builder = class {

    constructor(joi) {

        this.joi = joi;
    }

    parse(desc) {

        internals.validate(this.joi, desc);

        // Type

        let schema = this.joi[desc.type]()._bare();
        const def = schema._definition;

        // Flags

        if (desc.flags) {
            for (const flag in desc.flags) {
                const setter = def.flags[flag] && def.flags[flag].setter || flag;
                Assert(typeof schema[setter] === 'function', 'Invalid flag', flag, 'for type', desc.type);
                schema = schema[setter](this.build(desc.flags[flag]));
            }
        }

        // Preferences

        if (desc.preferences) {
            schema = schema.preferences(this.build(desc.preferences));
        }

        // Allow / Invalid

        if (desc.allow) {
            schema = schema.allow(...this.build(desc.allow));
        }

        if (desc.invalid) {
            schema = schema.invalid(...this.build(desc.invalid));
        }

        // Rules

        if (desc.rules) {
            for (const rule of desc.rules) {
                Assert(typeof schema[rule.name] === 'function', 'Invalid rule', rule.name, 'for type', desc.type);

                const args = [];
                if (rule.args) {
                    const built = {};
                    for (const key in rule.args) {
                        built[key] = this.build(rule.args[key], { assign: key });
                    }

                    const keys = Object.keys(built);
                    const definition = def.rules[rule.name].args;
                    if (definition) {
                        Assert(keys.length <= definition.length, 'Invalid number of arguments for', desc.type, rule.name, '(expected up to', definition.length, ', found', keys.length, ')');
                        for (const { name } of definition) {
                            args.push(built[name]);
                        }
                    }
                    else {
                        Assert(keys.length === 1, 'Invalid number of arguments for', desc.type, rule.name, '(expected up to 1, found', keys.length, ')');
                        args.push(built[keys[0]]);
                    }
                }

                // Apply

                schema = schema[rule.name](...args);

                // Ruleset

                const options = {};
                for (const custom in def.modifiers) {
                    if (rule[custom] !== undefined) {
                        options[custom] = this.build(rule[custom]);
                    }
                }

                if (Object.keys(options).length) {
                    schema = schema.rule(options);
                }
            }
        }

        // Terms

        const terms = {};
        for (const key in desc) {
            if (['allow', 'flags', 'invalid', 'whens', 'preferences', 'rules', 'type'].includes(key)) {
                continue;
            }

            Assert(def.terms[key], 'Term', key, 'missing configuration');
            const manifest = def.terms[key].manifest;

            if (manifest === 'schema') {
                terms[key] = desc[key].map((item) => this.parse(item));
                continue;
            }

            if (manifest === 'values') {
                terms[key] = desc[key].map((item) => this.build(item));
                continue;
            }

            if (manifest === 'single') {
                terms[key] = this.build(desc[key]);
                continue;
            }

            if (typeof manifest === 'object') {
                terms[key] = {};
                for (const name in desc[key]) {
                    const value = desc[key][name];
                    terms[key][name] = this.parse(value);
                }

                continue;
            }

            terms[key] = this.build(desc[key]);
        }

        if (desc.whens) {
            terms.whens = desc.whens.map((when) => this.build(when));
        }

        schema = def.manifest.build(schema, terms);
        schema.$_temp.ruleset = false;
        return schema;
    }

    build(desc, options = {}) {

        if (desc === null) {
            return null;
        }

        if (Array.isArray(desc)) {
            return desc.map((item) => this.build(item));
        }

        if (desc instanceof Error) {
            return desc;
        }

        if (options.assign === 'options') {
            return Clone(desc);
        }

        if (options.assign === 'regex') {
            return internals.regex(desc);
        }

        if (options.assign === 'ref') {
            return Ref.build(desc);
        }

        if (typeof desc !== 'object') {
            return desc;
        }

        if (Object.keys(desc).length === 1) {
            if (desc.buffer) {
                Assert(Buffer, 'Buffers are not supported');
                return Buffer && Buffer.from(desc.buffer, 'binary');                    // $lab:coverage:ignore$
            }

            if (desc.function) {
                return { [Common.symbols.literal]: true, literal: desc.function };
            }

            if (desc.override) {
                return Common.symbols.override;
            }

            if (desc.ref) {
                return Ref.build(desc.ref);
            }

            if (desc.regex) {
                return internals.regex(desc.regex);
            }

            if (desc.special) {
                Assert(['deep'].includes(desc.special), 'Unknown special value', desc.special);
                return Common.symbols.deepDefault;
            }

            if (desc.value) {
                return Clone(desc.value);
            }
        }

        if (desc.type) {
            return this.parse(desc);
        }

        if (desc.template) {
            return Template.build(desc);
        }

        const normalized = {};
        for (const key in desc) {
            normalized[key] = this.build(desc[key], { assign: key });
        }

        return normalized;
    }
};

internals.regex = function (string) {

    const end = string.lastIndexOf('/');
    const exp = string.slice(1, end);
    const flags = string.slice(end + 1);
    return new RegExp(exp, flags);
};

internals.validate = function (joi, desc) {

    Schemas = Schemas || require('./schemas');

    joi.assert(desc, Schemas.description);
};


code:
'use strict';

const Assert = require('@hapi/hoek/lib/assert');
const Clone = require('@hapi/hoek/lib/clone');

const Template = require('./template');

const internals = {};

exports.compile = function (messages, target) {

    // Single value string ('plain error message', 'template {error} message')

    if (typeof messages === 'string') {
        Assert(!target, 'Cannot set single message string');
        return new Template(messages);
    }

    // Single value template

    if (Template.isTemplate(messages)) {
        Assert(!target, 'Cannot set single message template');
        return messages;
    }

    // By error code { 'number.min': <string | template> }

    Assert(typeof messages === 'object' && !Array.isArray(messages), 'Invalid message options');

    target = target ? Clone(target) : {};

    for (let code in messages) {
        const message = messages[code];

        if (code === 'root' ||
            Template.isTemplate(message)) {

            target[code] = message;
            continue;
        }

        if (typeof message === 'string') {
            target[code] = new Template(message);
            continue;
        }

        // By language { english: { 'number.min': <string | template> } }

        Assert(typeof message === 'object' && !Array.isArray(message), 'Invalid message for', code);

        const language = code;
        target[language] = target[language] || {};

        for (code in message) {
            const localized = message[code];

            if (code === 'root' ||
                Template.isTemplate(localized)) {

                target[language][code] = localized;
                continue;
            }

            Assert(typeof localized === 'string', 'Invalid message for', code, 'in', language);
            target[language][code] = new Template(localized);
        }
    }

    return target;
};

exports.decompile = function (messages) {

    // By error code { 'number.min': <string | template> }

    const target = {};
    for (let code in messages) {
        const message = messages[code];

        if (code === 'root') {
            target.root = message;
            continue;
        }

        if (Template.isTemplate(message)) {
            target[code] = message.describe({ compact: true });
            continue;
        }

        // By language { english: { 'number.min': <string | template> } }

        const language = code;
        target[language] = {};

        for (code in message) {
            const localized = message[code];

            if (code === 'root') {
                target[language].root = localized;
                continue;
            }

            target[language][code] = localized.describe({ compact: true });
        }
    }

    return target;
};

exports.merge = function (base, extended) {

    if (!base) {
        return exports.compile(extended);
    }

    if (!extended) {
        return base;
    }

    // Single value string

    if (typeof extended === 'string') {
        return new Template(extended);
    }

    // Single value template

    if (Template.isTemplate(extended)) {
        return extended;
    }

    // By error code { 'number.min': <string | template> }

    const target = Clone(base);

    for (let code in extended) {
        const message = extended[code];

        if (code === 'root' ||
            Template.isTemplate(message)) {

            target[code] = message;
            continue;
        }

        if (typeof message === 'string') {
            target[code] = new Template(message);
            continue;
        }

        // By language { english: { 'number.min': <string | template> } }

        Assert(typeof message === 'object' && !Array.isArray(message), 'Invalid message for', code);

        const language = code;
        target[language] = target[language] || {};

        for (code in message) {
            const localized = message[code];

            if (code === 'root' ||
                Template.isTemplate(localized)) {

                target[language][code] = localized;
                continue;
            }

            Assert(typeof localized === 'string', 'Invalid message for', code, 'in', language);
            target[language][code] = new Template(localized);
        }
    }

    return target;
};

license: mit


code:
'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;
};


code:
'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);
    }
};


code:
'use strict';

const Joi = require('./index');

const internals = {};

// Preferences

internals.wrap = Joi.string()
    .min(1)
    .max(2)
    .allow(false);

exports.preferences = Joi.object({
    allowUnknown: Joi.boolean(),
    abortEarly: Joi.boolean(),
    artifacts: Joi.boolean(),
    cache: Joi.boolean(),
    context: Joi.object(),
    convert: Joi.boolean(),
    dateFormat: Joi.valid('date', 'iso', 'string', 'time', 'utc'),
    debug: Joi.boolean(),
    errors: {
        escapeHtml: Joi.boolean(),
        label: Joi.valid('path', 'key', false),
        language: [
            Joi.string(),
            Joi.object().ref()
        ],
        render: Joi.boolean(),
        stack: Joi.boolean(),
        wrap: {
            label: internals.wrap,
            array: internals.wrap,
            string: internals.wrap
        }
    },
    externals: Joi.boolean(),
    messages: Joi.object(),
    noDefaults: Joi.boolean(),
    nonEnumerables: Joi.boolean(),
    presence: Joi.valid('required', 'optional', 'forbidden'),
    skipFunctions: Joi.boolean(),
    stripUnknown: Joi.object({
        arrays: Joi.boolean(),
        objects: Joi.boolean()
    })
        .or('arrays', 'objects')
        .allow(true, false),
    warnings: Joi.boolean()
})
    .strict();

// Extensions

internals.nameRx = /^[a-zA-Z0-9]\w*$/;

internals.rule = Joi.object({
    alias: Joi.array().items(Joi.string().pattern(internals.nameRx)).single(),
    args: Joi.array().items(
        Joi.string(),
        Joi.object({
            name: Joi.string().pattern(internals.nameRx).required(),
            ref: Joi.boolean(),
            assert: Joi.alternatives([
                Joi.function(),
                Joi.object().schema()
            ])
                .conditional('ref', { is: true, then: Joi.required() }),
            normalize: Joi.function(),
            message: Joi.string().when('assert', { is: Joi.function(), then: Joi.required() })
        })
    ),
    convert: Joi.boolean(),
    manifest: Joi.boolean(),
    method: Joi.function().allow(false),
    multi: Joi.boolean(),
    validate: Joi.function()
});

exports.extension = Joi.object({
    type: Joi.alternatives([
        Joi.string(),
        Joi.object().regex()
    ])
        .required(),
    args: Joi.function(),
    cast: Joi.object().pattern(internals.nameRx, Joi.object({
        from: Joi.function().maxArity(1).required(),
        to: Joi.function().minArity(1).maxArity(2).required()
    })),
    base: Joi.object().schema()
        .when('type', { is: Joi.object().regex(), then: Joi.forbidden() }),
    coerce: [
        Joi.function().maxArity(3),
        Joi.object({ method: Joi.function().maxArity(3).required(), from: Joi.array().items(Joi.string()).single() })
    ],
    flags: Joi.object().pattern(internals.nameRx, Joi.object({
        setter: Joi.string(),
        default: Joi.any()
    })),
    manifest: {
        build: Joi.function().arity(2)
    },
    messages: [Joi.object(), Joi.string()],
    modifiers: Joi.object().pattern(internals.nameRx, Joi.function().minArity(1).maxArity(2)),
    overrides: Joi.object().pattern(internals.nameRx, Joi.function()),
    prepare: Joi.function().maxArity(3),
    rebuild: Joi.function().arity(1),
    rules: Joi.object().pattern(internals.nameRx, internals.rule),
    terms: Joi.object().pattern(internals.nameRx, Joi.object({
        init: Joi.array().allow(null).required(),
        manifest: Joi.object().pattern(/.+/, [
            Joi.valid('schema', 'single'),
            Joi.object({
                mapped: Joi.object({
                    from: Joi.string().required(),
                    to: Joi.string().required()
                })
                    .required()
            })
        ])
    })),
    validate: Joi.function().maxArity(3)
})
    .strict();

exports.extensions = Joi.array().items(Joi.object(), Joi.function().arity(1)).strict();

// Manifest

internals.desc = {

    buffer: Joi.object({
        buffer: Joi.string()
    }),

    func: Joi.object({
        function: Joi.function().required(),
        options: {
            literal: true
        }
    }),

    override: Joi.object({
        override: true
    }),

    ref: Joi.object({
        ref: Joi.object({
            type: Joi.valid('value', 'global', 'local'),
            path: Joi.array().required(),
            separator: Joi.string().length(1).allow(false),
            ancestor: Joi.number().min(0).integer().allow('root'),
            map: Joi.array().items(Joi.array().length(2)).min(1),
            adjust: Joi.function(),
            iterables: Joi.boolean(),
            in: Joi.boolean(),
            render: Joi.boolean()
        })
            .required()
    }),

    regex: Joi.object({
        regex: Joi.string().min(3)
    }),

    special: Joi.object({
        special: Joi.valid('deep').required()
    }),

    template: Joi.object({
        template: Joi.string().required(),
        options: Joi.object()
    }),

    value: Joi.object({
        value: Joi.alternatives([Joi.object(), Joi.array()]).required()
    })
};

internals.desc.entity = Joi.alternatives([
    Joi.array().items(Joi.link('...')),
    Joi.boolean(),
    Joi.function(),
    Joi.number(),
    Joi.string(),
    internals.desc.buffer,
    internals.desc.func,
    internals.desc.ref,
    internals.desc.regex,
    internals.desc.special,
    internals.desc.template,
    internals.desc.value,
    Joi.link('/')
]);

internals.desc.values = Joi.array()
    .items(
        null,
        Joi.boolean(),
        Joi.function(),
        Joi.number().allow(Infinity, -Infinity),
        Joi.string().allow(''),
        Joi.symbol(),
        internals.desc.buffer,
        internals.desc.func,
        internals.desc.override,
        internals.desc.ref,
        internals.desc.regex,
        internals.desc.template,
        internals.desc.value
    );

internals.desc.messages = Joi.object()
    .pattern(/.+/, [
        Joi.string(),
        internals.desc.template,
        Joi.object().pattern(/.+/, [Joi.string(), internals.desc.template])
    ]);

exports.description = Joi.object({
    type: Joi.string().required(),
    flags: Joi.object({
        cast: Joi.string(),
        default: Joi.any(),
        description: Joi.string(),
        empty: Joi.link('/'),
        failover: internals.desc.entity,
        id: Joi.string(),
        label: Joi.string(),
        only: true,
        presence: ['optional', 'required', 'forbidden'],
        result: ['raw', 'strip'],
        strip: Joi.boolean(),
        unit: Joi.string()
    })
        .unknown(),
    preferences: {
        allowUnknown: Joi.boolean(),
        abortEarly: Joi.boolean(),
        artifacts: Joi.boolean(),
        cache: Joi.boolean(),
        convert: Joi.boolean(),
        dateFormat: ['date', 'iso', 'string', 'time', 'utc'],
        errors: {
            escapeHtml: Joi.boolean(),
            label: ['path', 'key'],
            language: [
                Joi.string(),
                internals.desc.ref
            ],
            wrap: {
                label: internals.wrap,
                array: internals.wrap
            }
        },
        externals: Joi.boolean(),
        messages: internals.desc.messages,
        noDefaults: Joi.boolean(),
        nonEnumerables: Joi.boolean(),
        presence: ['required', 'optional', 'forbidden'],
        skipFunctions: Joi.boolean(),
        stripUnknown: Joi.object({
            arrays: Joi.boolean(),
            objects: Joi.boolean()
        })
            .or('arrays', 'objects')
            .allow(true, false),
        warnings: Joi.boolean()
    },
    allow: internals.desc.values,
    invalid: internals.desc.values,
    rules: Joi.array().min(1).items({
        name: Joi.string().required(),
        args: Joi.object().min(1),
        keep: Joi.boolean(),
        message: [
            Joi.string(),
            internals.desc.messages
        ],
        warn: Joi.boolean()
    }),

    // Terms

    keys: Joi.object().pattern(/.*/, Joi.link('/')),
    link: internals.desc.ref
})
    .pattern(/^[a-z]\w*$/, Joi.any());


code:
'use strict';

const Clone = require('@hapi/hoek/lib/clone');
const Reach = require('@hapi/hoek/lib/reach');

const Common = require('./common');

const internals = {
    value: Symbol('value')
};

module.exports = internals.State = class {

    constructor(path, ancestors, state) {

        this.path = path;
        this.ancestors = ancestors;                 // [parent, ..., root]

        this.mainstay = state.mainstay;
        this.schemas = state.schemas;               // [current, ..., root]
        this.debug = null;
    }

    localize(path, ancestors = null, schema = null) {

        const state = new internals.State(path, ancestors, this);

        if (schema &&
            state.schemas) {

            state.schemas = [internals.schemas(schema), ...state.schemas];
        }

        return state;
    }

    nest(schema, debug) {

        const state = new internals.State(this.path, this.ancestors, this);
        state.schemas = state.schemas && [internals.schemas(schema), ...state.schemas];
        state.debug = debug;
        return state;
    }

    shadow(value, reason) {

        this.mainstay.shadow = this.mainstay.shadow || new internals.Shadow();
        this.mainstay.shadow.set(this.path, value, reason);
    }

    snapshot() {

        if (this.mainstay.shadow) {
            this._snapshot = Clone(this.mainstay.shadow.node(this.path));
        }

        this.mainstay.snapshot();
    }

    restore() {

        if (this.mainstay.shadow) {
            this.mainstay.shadow.override(this.path, this._snapshot);
            this._snapshot = undefined;
        }

        this.mainstay.restore();
    }

    commit() {

        if (this.mainstay.shadow) {
            this.mainstay.shadow.override(this.path, this._snapshot);
            this._snapshot = undefined;
        }

        this.mainstay.commit();
    }
};

internals.schemas = function (schema) {

    if (Common.isSchema(schema)) {
        return { schema };
    }

    return schema;
};

internals.Shadow = class {

    constructor() {

        this._values = null;
    }

    set(path, value, reason) {

        if (!path.length) {                                     // No need to store root value
            return;
        }

        if (reason === 'strip' &&
            typeof path[path.length - 1] === 'number') {        // Cannot store stripped array values (due to shift)

            return;
        }

        this._values = this._values || new Map();

        let node = this._values;
        for (let i = 0; i < path.length; ++i) {
            const segment = path[i];
            let next = node.get(segment);
            if (!next) {
                next = new Map();
                node.set(segment, next);
            }

            node = next;
        }

        node[internals.value] = value;
    }

    get(path) {

        const node = this.node(path);
        if (node) {
            return node[internals.value];
        }
    }

    node(path) {

        if (!this._values) {
            return;
        }

        return Reach(this._values, path, { iterables: true });
    }

    override(path, node) {

        if (!this._values) {
            return;
        }

        const parents = path.slice(0, -1);
        const own = path[path.length - 1];
        const parent = Reach(this._values, parents, { iterables: true });

        if (node) {
            parent.set(own, node);
            return;
        }

        if (parent) {
            parent.delete(own);
        }
    }
};


code:
'use strict';

const Assert = require('@hapi/hoek/lib/assert');
const Clone = require('@hapi/hoek/lib/clone');
const EscapeHtml = require('@hapi/hoek/lib/escapeHtml');
const Formula = require('@sideway/formula');

const Common = require('./common');
const Errors = require('./errors');
const Ref = require('./ref');

const internals = {
    symbol: Symbol('template'),

    opens: new Array(1000).join('\u0000'),
    closes: new Array(1000).join('\u0001'),

    dateFormat: {
        date: Date.prototype.toDateString,
        iso: Date.prototype.toISOString,
        string: Date.prototype.toString,
        time: Date.prototype.toTimeString,
        utc: Date.prototype.toUTCString
    }
};

module.exports = exports = internals.Template = class {

    constructor(source, options) {

        Assert(typeof source === 'string', 'Template source must be a string');
        Assert(!source.includes('\u0000') && !source.includes('\u0001'), 'Template source cannot contain reserved control characters');

        this.source = source;
        this.rendered = source;

        this._template = null;

        if (options) {
            const { functions, ...opts } = options;
            this._settings = Object.keys(opts).length ? Clone(opts) : undefined;
            this._functions = functions;
            if (this._functions) {
                Assert(Object.keys(this._functions).every((key) => typeof key === 'string'), 'Functions keys must be strings');
                Assert(Object.values(this._functions).every((key) => typeof key === 'function'), 'Functions values must be functions');
            }
        }
        else {
            this._settings = undefined;
            this._functions = undefined;
        }

        this._parse();
    }

    _parse() {

        // 'text {raw} {{ref}} \\{{ignore}} {{ignore\\}} {{ignore {{ignore}'

        if (!this.source.includes('{')) {
            return;
        }

        // Encode escaped \\{{{{{

        const encoded = internals.encode(this.source);

        // Split on first { in each set

        const parts = internals.split(encoded);

        // Process parts

        let refs = false;
        const processed = [];
        const head = parts.shift();
        if (head) {
            processed.push(head);
        }

        for (const part of parts) {
            const raw = part[0] !== '{';
            const ender = raw ? '}' : '}}';
            const end = part.indexOf(ender);
            if (end === -1 ||                               // Ignore non-matching closing
                part[1] === '{') {                          // Ignore more than two {

                processed.push(`{${internals.decode(part)}`);
                continue;
            }

            let variable = part.slice(raw ? 0 : 1, end);
            const wrapped = variable[0] === ':';
            if (wrapped) {
                variable = variable.slice(1);
            }

            const dynamic = this._ref(internals.decode(variable), { raw, wrapped });
            processed.push(dynamic);
            if (typeof dynamic !== 'string') {
                refs = true;
            }

            const rest = part.slice(end + ender.length);
            if (rest) {
                processed.push(internals.decode(rest));
            }
        }

        if (!refs) {
            this.rendered = processed.join('');
            return;
        }

        this._template = processed;
    }

    static date(date, prefs) {

        return internals.dateFormat[prefs.dateFormat].call(date);
    }

    describe(options = {}) {

        if (!this._settings &&
            options.compact) {

            return this.source;
        }

        const desc = { template: this.source };
        if (this._settings) {
            desc.options = this._settings;
        }

        if (this._functions) {
            desc.functions = this._functions;
        }

        return desc;
    }

    static build(desc) {

        return new internals.Template(desc.template, desc.options || desc.functions ? { ...desc.options, functions: desc.functions } : undefined);
    }

    isDynamic() {

        return !!this._template;
    }

    static isTemplate(template) {

        return template ? !!template[Common.symbols.template] : false;
    }

    refs() {

        if (!this._template) {
            return;
        }

        const refs = [];
        for (const part of this._template) {
            if (typeof part !== 'string') {
                refs.push(...part.refs);
            }
        }

        return refs;
    }

    resolve(value, state, prefs, local) {

        if (this._template &&
            this._template.length === 1) {

            return this._part(this._template[0], /* context -> [*/ value, state, prefs, local, {} /*] */);
        }

        return this.render(value, state, prefs, local);
    }

    _part(part, ...args) {

        if (part.ref) {
            return part.ref.resolve(...args);
        }

        return part.formula.evaluate(args);
    }

    render(value, state, prefs, local, options = {}) {

        if (!this.isDynamic()) {
            return this.rendered;
        }

        const parts = [];
        for (const part of this._template) {
            if (typeof part === 'string') {
                parts.push(part);
            }
            else {
                const rendered = this._part(part, /* context -> [*/ value, state, prefs, local, options /*] */);
                const string = internals.stringify(rendered, value, state, prefs, local, options);
                if (string !== undefined) {
                    const result = part.raw || (options.errors && options.errors.escapeHtml) === false ? string : EscapeHtml(string);
                    parts.push(internals.wrap(result, part.wrapped && prefs.errors.wrap.label));
                }
            }
        }

        return parts.join('');
    }

    _ref(content, { raw, wrapped }) {

        const refs = [];
        const reference = (variable) => {

            const ref = Ref.create(variable, this._settings);
            refs.push(ref);
            return (context) => {

                const resolved = ref.resolve(...context);
                return resolved !== undefined ? resolved : null;
            };
        };

        try {
            const functions = this._functions ? { ...internals.functions, ...this._functions } : internals.functions;
            var formula = new Formula.Parser(content, { reference, functions, constants: internals.constants });
        }
        catch (err) {
            err.message = `Invalid template variable "${content}" fails due to: ${err.message}`;
            throw err;
        }

        if (formula.single) {
            if (formula.single.type === 'reference') {
                const ref = refs[0];
                return { ref, raw, refs, wrapped: wrapped || ref.type === 'local' && ref.key === 'label' };
            }

            return internals.stringify(formula.single.value);
        }

        return { formula, raw, refs };
    }

    toString() {

        return this.source;
    }
};

internals.Template.prototype[Common.symbols.template] = true;
internals.Template.prototype.isImmutable = true;                // Prevents Hoek from deep cloning schema objects

internals.encode = function (string) {

    return string
        .replace(/\\(\{+)/g, ($0, $1) => {

            return internals.opens.slice(0, $1.length);
        })
        .replace(/\\(\}+)/g, ($0, $1) => {

            return internals.closes.slice(0, $1.length);
        });
};

internals.decode = function (string) {

    return string
        .replace(/\u0000/g, '{')
        .replace(/\u0001/g, '}');
};

internals.split = function (string) {

    const parts = [];
    let current = '';

    for (let i = 0; i < string.length; ++i) {
        const char = string[i];

        if (char === '{') {
            let next = '';
            while (i + 1 < string.length &&
                string[i + 1] === '{') {

                next += '{';
                ++i;
            }

            parts.push(current);
            current = next;
        }
        else {
            current += char;
        }
    }

    parts.push(current);
    return parts;
};

internals.wrap = function (value, ends) {

    if (!ends) {
        return value;
    }

    if (ends.length === 1) {
        return `${ends}${value}${ends}`;
    }

    return `${ends[0]}${value}${ends[1]}`;
};

internals.stringify = function (value, original, state, prefs, local, options = {}) {

    const type = typeof value;
    const wrap = prefs && prefs.errors && prefs.errors.wrap || {};

    let skipWrap = false;
    if (Ref.isRef(value) &&
        value.render) {

        skipWrap = value.in;
        value = value.resolve(original, state, prefs, local, { in: value.in, ...options });
    }

    if (value === null) {
        return 'null';
    }

    if (type === 'string') {
        return internals.wrap(value, options.arrayItems && wrap.string);
    }

    if (type === 'number' ||
        type === 'function' ||
        type === 'symbol') {

        return value.toString();
    }

    if (type !== 'object') {
        return JSON.stringify(value);
    }

    if (value instanceof Date) {
        return internals.Template.date(value, prefs);
    }

    if (value instanceof Map) {
        const pairs = [];
        for (const [key, sym] of value.entries()) {
            pairs.push(`${key.toString()} -> ${sym.toString()}`);
        }

        value = pairs;
    }

    if (!Array.isArray(value)) {
        return value.toString();
    }

    const values = [];
    for (const item of value) {
        values.push(internals.stringify(item, original, state, prefs, local, { arrayItems: true, ...options }));
    }

    return internals.wrap(values.join(', '), !skipWrap && wrap.array);
};

internals.constants = {

    true: true,
    false: false,
    null: null,

    second: 1000,
    minute: 60 * 1000,
    hour: 60 * 60 * 1000,
    day: 24 * 60 * 60 * 1000
};

internals.functions = {

    if(condition, then, otherwise) {

        return condition ? then : otherwise;
    },

    length(item) {

        if (typeof item === 'string') {
            return item.length;
        }

        if (!item || typeof item !== 'object') {
            return null;
        }

        if (Array.isArray(item)) {
            return item.length;
        }

        return Object.keys(item).length;
    },

    msg(code) {

        const [value, state, prefs, local, options] = this;
        const messages = options.messages;
        if (!messages) {
            return '';
        }

        const template = Errors.template(value, messages[0], code, state, prefs) || Errors.template(value, messages[1], code, state, prefs);
        if (!template) {
            return '';
        }

        return template.render(value, state, prefs, local, options);
    },

    number(value) {

        if (typeof value === 'number') {
            return value;
        }

        if (typeof value === 'string') {
            return parseFloat(value);
        }

        if (typeof value === 'boolean') {
            return value ? 1 : 0;
        }

        if (value instanceof Date) {
            return value.getTime();
        }

        return null;
    }
};


code:
'use strict';

const DeepEqual = require('@hapi/hoek/lib/deepEqual');
const Pinpoint = require('@sideway/pinpoint');

const Errors = require('./errors');

const internals = {
    codes: {
        error: 1,
        pass: 2,
        full: 3
    },
    labels: {
        0: 'never used',
        1: 'always error',
        2: 'always pass'
    }
};

exports.setup = function (root) {

    const trace = function () {

        root._tracer = root._tracer || new internals.Tracer();
        return root._tracer;
    };

    root.trace = trace;
    root[Symbol.for('@hapi/lab/coverage/initialize')] = trace;

    root.untrace = () => {

        root._tracer = null;
    };
};

exports.location = function (schema) {

    return schema.$_setFlag('_tracerLocation', Pinpoint.location(2));                       // base.tracer(), caller
};

internals.Tracer = class {

    constructor() {

        this.name = 'Joi';
        this._schemas = new Map();
    }

    _register(schema) {

        const existing = this._schemas.get(schema);
        if (existing) {
            return existing.store;
        }

        const store = new internals.Store(schema);
        const { filename, line } = schema._flags._tracerLocation || Pinpoint.location(5);   // internals.tracer(), internals.entry(), exports.entry(), validate(), caller
        this._schemas.set(schema, { filename, line, store });
        return store;
    }

    _combine(merged, sources) {

        for (const { store } of this._schemas.values()) {
            store._combine(merged, sources);
        }
    }

    report(file) {

        const coverage = [];

        // Process each registered schema

        for (const { filename, line, store } of this._schemas.values()) {
            if (file &&
                file !== filename) {

                continue;
            }

            // Process sub schemas of the registered root

            const missing = [];
            const skipped = [];

            for (const [schema, log] of store._sources.entries()) {

                // Check if sub schema parent skipped

                if (internals.sub(log.paths, skipped)) {
                    continue;
                }

                // Check if sub schema reached

                if (!log.entry) {
                    missing.push({
                        status: 'never reached',
                        paths: [...log.paths]
                    });

                    skipped.push(...log.paths);
                    continue;
                }

                // Check values

                for (const type of ['valid', 'invalid']) {
                    const set = schema[`_${type}s`];
                    if (!set) {
                        continue;
                    }

                    const values = new Set(set._values);
                    const refs = new Set(set._refs);
                    for (const { value, ref } of log[type]) {
                        values.delete(value);
                        refs.delete(ref);
                    }

                    if (values.size ||
                        refs.size) {

                        missing.push({
                            status: [...values, ...[...refs].map((ref) => ref.display)],
                            rule: `${type}s`
                        });
                    }
                }

                // Check rules status

                const rules = schema._rules.map((rule) => rule.name);
                for (const type of ['default', 'failover']) {
                    if (schema._flags[type] !== undefined) {
                        rules.push(type);
                    }
                }

                for (const name of rules) {
                    const status = internals.labels[log.rule[name] || 0];
                    if (status) {
                        const report = { rule: name, status };
                        if (log.paths.size) {
                            report.paths = [...log.paths];
                        }

                        missing.push(report);
                    }
                }
            }

            if (missing.length) {
                coverage.push({
                    filename,
                    line,
                    missing,
                    severity: 'error',
                    message: `Schema missing tests for ${missing.map(internals.message).join(', ')}`
                });
            }
        }

        return coverage.length ? coverage : null;
    }
};

internals.Store = class {

    constructor(schema) {

        this.active = true;
        this._sources = new Map();          // schema -> { paths, entry, rule, valid, invalid }
        this._combos = new Map();           // merged -> [sources]
        this._scan(schema);
    }

    debug(state, source, name, result) {

        state.mainstay.debug && state.mainstay.debug.push({ type: source, name, result, path: state.path });
    }

    entry(schema, state) {

        internals.debug(state, { type: 'entry' });

        this._record(schema, (log) => {

            log.entry = true;
        });
    }

    filter(schema, state, source, value) {

        internals.debug(state, { type: source, ...value });

        this._record(schema, (log) => {

            log[source].add(value);
        });
    }

    log(schema, state, source, name, result) {

        internals.debug(state, { type: source, name, result: result === 'full' ? 'pass' : result });

        this._record(schema, (log) => {

            log[source][name] = log[source][name] || 0;
            log[source][name] |= internals.codes[result];
        });
    }

    resolve(state, ref, to) {

        if (!state.mainstay.debug) {
            return;
        }

        const log = { type: 'resolve', ref: ref.display, to, path: state.path };
        state.mainstay.debug.push(log);
    }

    value(state, by, from, to, name) {

        if (!state.mainstay.debug ||
            DeepEqual(from, to)) {

            return;
        }

        const log = { type: 'value', by, from, to, path: state.path };
        if (name) {
            log.name = name;
        }

        state.mainstay.debug.push(log);
    }

    _record(schema, each) {

        const log = this._sources.get(schema);
        if (log) {
            each(log);
            return;
        }

        const sources = this._combos.get(schema);
        for (const source of sources) {
            this._record(source, each);
        }
    }

    _scan(schema, _path) {

        const path = _path || [];

        let log = this._sources.get(schema);
        if (!log) {
            log = {
                paths: new Set(),
                entry: false,
                rule: {},
                valid: new Set(),
                invalid: new Set()
            };

            this._sources.set(schema, log);
        }

        if (path.length) {
            log.paths.add(path);
        }

        const each = (sub, source) => {

            const subId = internals.id(sub, source);
            this._scan(sub, path.concat(subId));
        };

        schema.$_modify({ each, ref: false });
    }

    _combine(merged, sources) {

        this._combos.set(merged, sources);
    }
};

internals.message = function (item) {

    const path = item.paths ? Errors.path(item.paths[0]) + (item.rule ? ':' : '') : '';
    return `${path}${item.rule || ''} (${item.status})`;
};

internals.id = function (schema, { source, name, path, key }) {

    if (schema._flags.id) {
        return schema._flags.id;
    }

    if (key) {
        return key;
    }

    name = `@${name}`;

    if (source === 'terms') {
        return [name, path[Math.min(path.length - 1, 1)]];
    }

    return name;
};

internals.sub = function (paths, skipped) {

    for (const path of paths) {
        for (const skip of skipped) {
            if (DeepEqual(path.slice(0, skip.length), skip)) {
                return true;
            }
        }
    }

    return false;
};

internals.debug = function (state, event) {

    if (state.mainstay.debug) {
        event.path = state.debug ? [...state.path, state.debug] : state.path;
        state.mainstay.debug.push(event);
    }
};


code:
'use strict';

const Assert = require('@hapi/hoek/lib/assert');
const Merge = require('@hapi/hoek/lib/merge');

const Any = require('./any');
const Common = require('../common');
const Compile = require('../compile');
const Errors = require('../errors');
const Ref = require('../ref');

const internals = {};

module.exports = Any.extend({

    type: 'alternatives',

    flags: {

        match: { default: 'any' }                 // 'any', 'one', 'all'
    },

    terms: {

        matches: { init: [], register: Ref.toSibling }
    },

    args(schema, ...schemas) {

        if (schemas.length === 1) {
            if (Array.isArray(schemas[0])) {
                return schema.try(...schemas[0]);
            }
        }

        return schema.try(...schemas);
    },

    validate(value, helpers) {

        const { schema, error, state, prefs } = helpers;

        // Match all or one

        if (schema._flags.match) {
            const matched = [];
            const failed = [];

            for (let i = 0; i < schema.$_terms.matches.length; ++i) {
                const item = schema.$_terms.matches[i];
                const localState = state.nest(item.schema, `match.${i}`);
                localState.snapshot();

                const result = item.schema.$_validate(value, localState, prefs);
                if (!result.errors) {
                    matched.push(result.value);
                    localState.commit();
                }
                else {
                    failed.push(result.errors);
                    localState.restore();
                }
            }

            if (matched.length === 0) {
                const context = {
                    details: failed.map((f) => Errors.details(f, { override: false }))
                };

                return { errors: error('alternatives.any', context) };
            }

            // Match one

            if (schema._flags.match === 'one') {
                return matched.length === 1 ? { value: matched[0] } : { errors: error('alternatives.one') };
            }

            // Match all

            if (matched.length !== schema.$_terms.matches.length) {
                const context = {
                    details: failed.map((f) => Errors.details(f, { override: false }))
                };

                return { errors: error('alternatives.all', context) };
            }

            const isAnyObj = (alternative) => {

                return alternative.$_terms.matches.some((v) => {

                    return v.schema.type === 'object' ||
                        (v.schema.type === 'alternatives' && isAnyObj(v.schema));
                });
            };

            return isAnyObj(schema) ? { value: matched.reduce((acc, v) => Merge(acc, v, { mergeArrays: false })) } : { value: matched[matched.length - 1] };
        }

        // Match any

        const errors = [];
        for (let i = 0; i < schema.$_terms.matches.length; ++i) {
            const item = schema.$_terms.matches[i];

            // Try

            if (item.schema) {
                const localState = state.nest(item.schema, `match.${i}`);
                localState.snapshot();

                const result = item.schema.$_validate(value, localState, prefs);
                if (!result.errors) {
                    localState.commit();
                    return result;
                }

                localState.restore();
                errors.push({ schema: item.schema, reports: result.errors });
                continue;
            }

            // Conditional

            const input = item.ref ? item.ref.resolve(value, state, prefs) : value;
            const tests = item.is ? [item] : item.switch;

            for (let j = 0; j < tests.length; ++j) {
                const test = tests[j];
                const { is, then, otherwise } = test;

                const id = `match.${i}${item.switch ? '.' + j : ''}`;
                if (!is.$_match(input, state.nest(is, `${id}.is`), prefs)) {
                    if (otherwise) {
                        return otherwise.$_validate(value, state.nest(otherwise, `${id}.otherwise`), prefs);
                    }
                }
                else if (then) {
                    return then.$_validate(value, state.nest(then, `${id}.then`), prefs);
                }
            }
        }

        return internals.errors(errors, helpers);
    },

    rules: {

        conditional: {
            method(condition, options) {

                Assert(!this._flags._endedSwitch, 'Unreachable condition');
                Assert(!this._flags.match, 'Cannot combine match mode', this._flags.match, 'with conditional rule');
                Assert(options.break === undefined, 'Cannot use break option with alternatives conditional');

                const obj = this.clone();

                const match = Compile.when(obj, condition, options);
                const conditions = match.is ? [match] : match.switch;
                for (const item of conditions) {
                    if (item.then &&
                        item.otherwise) {

                        obj.$_setFlag('_endedSwitch', true, { clone: false });
                        break;
                    }
                }

                obj.$_terms.matches.push(match);
                return obj.$_mutateRebuild();
            }
        },

        match: {
            method(mode) {

                Assert(['any', 'one', 'all'].includes(mode), 'Invalid alternatives match mode', mode);

                if (mode !== 'any') {
                    for (const match of this.$_terms.matches) {
                        Assert(match.schema, 'Cannot combine match mode', mode, 'with conditional rules');
                    }
                }

                return this.$_setFlag('match', mode);
            }
        },

        try: {
            method(...schemas) {

                Assert(schemas.length, 'Missing alternative schemas');
                Common.verifyFlat(schemas, 'try');

                Assert(!this._flags._endedSwitch, 'Unreachable condition');

                const obj = this.clone();
                for (const schema of schemas) {
                    obj.$_terms.matches.push({ schema: obj.$_compile(schema) });
                }

                return obj.$_mutateRebuild();
            }
        }
    },

    overrides: {

        label(name) {

            const obj = this.$_parent('label', name);
            const each = (item, source) => {

                return source.path[0] !== 'is' && typeof item._flags.label !== 'string' ? item.label(name) : undefined;
            };

            return obj.$_modify({ each, ref: false });
        }
    },

    rebuild(schema) {

        // Flag when an alternative type is an array

        const each = (item) => {

            if (Common.isSchema(item) &&
                item.type === 'array') {

                schema.$_setFlag('_arrayItems', true, { clone: false });
            }
        };

        schema.$_modify({ each });
    },

    manifest: {

        build(obj, desc) {

            if (desc.matches) {
                for (const match of desc.matches) {
                    const { schema, ref, is, not, then, otherwise } = match;
                    if (schema) {
                        obj = obj.try(schema);
                    }
                    else if (ref) {
                        obj = obj.conditional(ref, { is, then, not, otherwise, switch: match.switch });
                    }
                    else {
                        obj = obj.conditional(is, { then, otherwise });
                    }
                }
            }

            return obj;
        }
    },

    messages: {
        'alternatives.all': '{{#label}} does not match all of the required types',
        'alternatives.any': '{{#label}} does not match any of the allowed types',
        'alternatives.match': '{{#label}} does not match any of the allowed types',
        'alternatives.one': '{{#label}} matches more than one allowed type',
        'alternatives.types': '{{#label}} must be one of {{#types}}'
    }
});

// Helpers

internals.errors = function (failures, { error, state }) {

    // Nothing matched due to type criteria rules

    if (!failures.length) {
        return { errors: error('alternatives.any') };
    }

    // Single error

    if (failures.length === 1) {
        return { errors: failures[0].reports };
    }

    // Analyze reasons

    const valids = new Set();
    const complex = [];

    for (const { reports, schema } of failures) {

        // Multiple errors (!abortEarly)

        if (reports.length > 1) {
            return internals.unmatched(failures, error);
        }

        // Custom error

        const report = reports[0];
        if (report instanceof Errors.Report === false) {
            return internals.unmatched(failures, error);
        }

        // Internal object or array error

        if (report.state.path.length !== state.path.length) {
            complex.push({ type: schema.type, report });
            continue;
        }

        // Valids

        if (report.code === 'any.only') {
            for (const valid of report.local.valids) {
                valids.add(valid);
            }

            continue;
        }

        // Base type

        const [type, code] = report.code.split('.');
        if (code !== 'base') {
            complex.push({ type: schema.type, report });
        }
        else if (report.code === 'object.base') {
            valids.add(report.local.type);
        }
        else {
            valids.add(type);
        }
    }

    // All errors are base types or valids

    if (!complex.length) {
        return { errors: error('alternatives.types', { types: [...valids] }) };
    }

    // Single complex error

    if (complex.length === 1) {
        return { errors: complex[0].report };
    }

    return internals.unmatched(failures, error);
};

internals.unmatched = function (failures, error) {

    const errors = [];
    for (const failure of failures) {
        errors.push(...failure.reports);
    }

    return { errors: error('alternatives.match', Errors.details(errors, { override: false })) };
};


code:
'use strict';

const Assert = require('@hapi/hoek/lib/assert');

const Base = require('../base');
const Common = require('../common');
const Messages = require('../messages');

const internals = {};

module.exports = Base.extend({

    type: 'any',

    flags: {

        only: { default: false }
    },

    terms: {

        alterations: { init: null },
        examples: { init: null },
        externals: { init: null },
        metas: { init: [] },
        notes: { init: [] },
        shared: { init: null },
        tags: { init: [] },
        whens: { init: null }
    },

    rules: {

        custom: {
            method(method, description) {

                Assert(typeof method === 'function', 'Method must be a function');
                Assert(description === undefined || description && typeof description === 'string', 'Description must be a non-empty string');

                return this.$_addRule({ name: 'custom', args: { method, description } });
            },
            validate(value, helpers, { method }) {

                try {
                    return method(value, helpers);
                }
                catch (err) {
                    return helpers.error('any.custom', { error: err });
                }
            },
            args: ['method', 'description'],
            multi: true
        },

        messages: {
            method(messages) {

                return this.prefs({ messages });
            }
        },

        shared: {
            method(schema) {

                Assert(Common.isSchema(schema) && schema._flags.id, 'Schema must be a schema with an id');

                const obj = this.clone();
                obj.$_terms.shared = obj.$_terms.shared || [];
                obj.$_terms.shared.push(schema);
                obj.$_mutateRegister(schema);
                return obj;
            }
        },

        warning: {
            method(code, local) {

                Assert(code && typeof code === 'string', 'Invalid warning code');

                return this.$_addRule({ name: 'warning', args: { code, local }, warn: true });
            },
            validate(value, helpers, { code, local }) {

                return helpers.error(code, local);
            },
            args: ['code', 'local'],
            multi: true
        }
    },

    modifiers: {

        keep(rule, enabled = true) {

            rule.keep = enabled;
        },

        message(rule, message) {

            rule.message = Messages.compile(message);
        },

        warn(rule, enabled = true) {

            rule.warn = enabled;
        }
    },

    manifest: {

        build(obj, desc) {

            for (const key in desc) {
                const values = desc[key];

                if (['examples', 'externals', 'metas', 'notes', 'tags'].includes(key)) {
                    for (const value of values) {
                        obj = obj[key.slice(0, -1)](value);
                    }

                    continue;
                }

                if (key === 'alterations') {
                    const alter = {};
                    for (const { target, adjuster } of values) {
                        alter[target] = adjuster;
                    }

                    obj = obj.alter(alter);
                    continue;
                }

                if (key === 'whens') {
                    for (const value of values) {
                        const { ref, is, not, then, otherwise, concat } = value;
                        if (concat) {
                            obj = obj.concat(concat);
                        }
                        else if (ref) {
                            obj = obj.when(ref, { is, not, then, otherwise, switch: value.switch, break: value.break });
                        }
                        else {
                            obj = obj.when(is, { then, otherwise, break: value.break });
                        }
                    }

                    continue;
                }

                if (key === 'shared') {
                    for (const value of values) {
                        obj = obj.shared(value);
                    }
                }
            }

            return obj;
        }
    },

    messages: {
        'any.custom': '{{#label}} failed custom validation because {{#error.message}}',
        'any.default': '{{#label}} threw an error when running default method',
        'any.failover': '{{#label}} threw an error when running failover method',
        'any.invalid': '{{#label}} contains an invalid value',
        'any.only': '{{#label}} must be {if(#valids.length == 1, "", "one of ")}{{#valids}}',
        'any.ref': '{{#label}} {{#arg}} references {{:#ref}} which {{#reason}}',
        'any.required': '{{#label}} is required',
        'any.unknown': '{{#label}} is not allowed'
    }
});