Skip to content

Commit

Permalink
Rewrite refs. Closes #1830. Closes #1831. Closes #1832. Closes #1833. C…
Browse files Browse the repository at this point in the history
…loses #1650.
  • Loading branch information
hueniverse committed Jun 1, 2019
1 parent f3ffef2 commit 7e86c05
Show file tree
Hide file tree
Showing 23 changed files with 825 additions and 264 deletions.
203 changes: 124 additions & 79 deletions API.md

Large diffs are not rendered by default.

17 changes: 9 additions & 8 deletions lib/about.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,26 @@ exports.describe = function (schema) {
if (internals.flags.some((flag) => schema._flags.hasOwnProperty(flag))) {
description.flags = {};
for (const flag of flags) {
const value = schema._flags[flag];
if (flag === 'empty') {
description.flags[flag] = schema._flags[flag].describe();
description.flags[flag] = value.describe();
}
else if (flag === 'default') {
if (Ref.isRef(schema._flags[flag])) {
description.flags[flag] = schema._flags[flag].describe();
if (Ref.isRef(value)) {
description.flags[flag] = value.describe();
}
else if (typeof schema._flags[flag] === 'function') {
else if (typeof value === 'function') {
description.flags[flag] = {
description: schema._flags[flag].description,
function: schema._flags[flag]
description: value.description,
function: value
};
}
else {
description.flags[flag] = schema._flags[flag];
description.flags[flag] = value;
}
}
else if (!internals.exclude.includes(flag)) {
description.flags[flag] = schema._flags[flag];
description.flags[flag] = value;
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions lib/cast.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ exports.schema = function (Joi, config, options = {}) {
config !== null &&
typeof config === 'object') {

if (Ref.isRef(config)) {
return Joi.valid(config);
}

if (config.isJoi) {
return config;
}
Expand Down Expand Up @@ -49,10 +53,6 @@ exports.schema = function (Joi, config, options = {}) {
return Joi.boolean().valid(config);
}

if (Ref.isRef(config)) {
return Joi.valid(config);
}

Hoek.assert(config === null, 'Invalid schema content:', config);

return Joi.valid(null);
Expand All @@ -76,5 +76,5 @@ internals.appendPath = function (Joi, config) {

exports.ref = function (id) {

return Ref.isRef(id) ? id : Ref.create(id);
return Ref.isRef(id) ? id : new Ref(id);
};
7 changes: 6 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,19 @@ internals.root = function () {

root.ref = function (...args) {

return Ref.create(...args);
return new Ref(...args);
};

root.isRef = function (ref) {

return Ref.isRef(ref);
};

root.isSchema = function (schema) {

return Utils.isSchema(schema);
};

root.validate = function (value, ...args /*, [schema], [options], callback */) {

const last = args[args.length - 1];
Expand Down
28 changes: 14 additions & 14 deletions lib/language.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,30 +71,28 @@ exports.errors = {
function: {
base: 'must be a Function',
arity: 'must have an arity of {{n}}',
minArity: 'must have an arity greater or equal to {{n}}',
class: 'must be a class',
maxArity: 'must have an arity lesser or equal to {{n}}',
ref: 'must be a Joi reference',
class: 'must be a class'
minArity: 'must have an arity greater or equal to {{n}}'
},
lazy: {
base: '!!schema error: lazy schema must be set',
schema: '!!schema error: lazy schema function must return a schema'
},
object: {
base: 'must be an object',
allowUnknown: '!!"{{!child}}" is not allowed',
and: 'contains {{presentWithLabels}} without its required peers {{missingWithLabels}}',
assert: '!!"{{ref}}" validation failed because "{{ref}}" failed to {{message}}',
child: '!!child "{{!child}}" fails because {{reason}}',
min: 'must have at least {{limit}} children',
max: 'must have less than or equal to {{limit}} children',
length: 'must have {{limit}} children',
allowUnknown: '!!"{{!child}}" is not allowed',
with: '!!"{{mainWithLabel}}" missing required peer "{{peerWithLabel}}"',
without: '!!"{{mainWithLabel}}" conflict with forbidden peer "{{peerWithLabel}}"',
max: 'must have less than or equal to {{limit}} children',
min: 'must have at least {{limit}} children',
missing: 'must contain at least one of {{peersWithLabels}}',
xor: 'contains a conflict between exclusive peers {{peersWithLabels}}',
oxor: 'contains a conflict between optional exclusive peers {{peersWithLabels}}',
and: 'contains {{presentWithLabels}} without its required peers {{missingWithLabels}}',
nand: '!!"{{mainWithLabel}}" must not exist simultaneously with {{peersWithLabels}}',
assert: '!!"{{ref}}" validation failed because "{{ref}}" failed to {{message}}',
oxor: 'contains a conflict between optional exclusive peers {{peersWithLabels}}',
ref: 'references "{{ref}}" which is not a positive integer',
refType: 'must be a Joi reference',
rename: {
multiple: 'cannot rename child "{{from}}" because multiple renames are disabled and another key was already renamed to "{{to}}"',
override: 'cannot rename child "{{from}}" because override is disabled and target "{{to}}" exists',
Expand All @@ -103,9 +101,11 @@ exports.errors = {
override: 'cannot rename children {{from}} because override is disabled and target "{{to}}" exists'
}
},
type: 'must be an instance of "{{type}}"',
schema: 'must be a Joi instance',
ref: 'references "{{ref}}" which is not a positive integer'
type: 'must be an instance of "{{type}}"',
with: '!!"{{mainWithLabel}}" missing required peer "{{peerWithLabel}}"',
without: '!!"{{mainWithLabel}}" conflict with forbidden peer "{{peerWithLabel}}"',
xor: 'contains a conflict between exclusive peers {{peersWithLabels}}'
},
number: {
base: 'must be a number',
Expand Down
168 changes: 133 additions & 35 deletions lib/ref.js
Original file line number Diff line number Diff line change
@@ -1,69 +1,167 @@
'use strict';

const Hoek = require('@hapi/hoek');
const Marker = require('@hapi/marker');

const Utils = require('./utils');

const internals = {};

const internals = {
symbol: Marker('ref') // Used to internally identify references (shared with other joi versions)
};


module.exports = exports = internals.Ref = class {

exports.create = function (key, options) {
constructor(key, options = {}) {

Hoek.assert(typeof key === 'string', 'Invalid reference key:', key);
Hoek.assert(typeof key === 'string', 'Invalid reference key:', key);

const settings = options ? Hoek.clone(options) : {}; // options can be reused and modified
this.settings = Hoek.clone(options);
this[internals.symbol] = true;

const ref = function (parent, value, validationOptions) {
const contextPrefix = this.settings.contextPrefix || '$';
const separator = this.settings.separator || '.';

const target = ref.isContext ? validationOptions.context : (settings.self ? value : parent);
return Hoek.reach(target, ref.key, settings);
};
this.isContext = key[0] === contextPrefix;
if (this.isContext) {
key = key.slice(1);
this.display = `context:${key}`;
}
else {
if (this.settings.ancestor !== undefined) {
Hoek.assert(key[0] !== separator, 'Cannot combine prefix with ancestor option');
}
else {
const [ancestor, slice] = internals.ancestor(key, separator);
if (slice) {
key = key.slice(slice);
}

const contextPrefix = settings.contextPrefix || '$';
const separator = settings.separator || '.';
this.settings.ancestor = ancestor;
}

ref.isContext = key[0] === contextPrefix;
if (!ref.isContext &&
key[0] === separator) {
this.display = internals.display(key, separator, this.settings.ancestor);
}

key = key.slice(1);
settings.self = true;
this.key = key;
this.ancestor = this.settings.ancestor;
this.path = this.key.split(separator);
this.depth = this.path.length;
this.root = this.path[0];
}

ref.key = ref.isContext ? key.slice(1) : key;
ref.path = ref.key.split(separator);
ref.depth = ref.path.length;
ref.root = ref.path[0];
ref.isJoi = true;
resolve(value, state, options = {}) {

ref.toString = () => {
const ancestor = this.settings.ancestor;
Hoek.assert(!ancestor || ancestor <= state.ancestors.length, 'Invalid reference exceeds the schema root:', this.display);

return (ref.isContext ? 'context:' : 'ref:') + ref.key;
};
const target = this.isContext ? options.context : (ancestor ? state.ancestors[ancestor - 1] : value);
return Hoek.reach(target, this.key, this.settings);
}

toString() {

ref.describe = () => {
return this.display;
}

describe() {

return {
type: ref.isContext ? 'context' : 'ref',
key: ref.key,
path: ref.path
type: this.isContext ? 'context' : 'ref',
key: this.key,
path: this.path
};
};
}

static isRef(ref) {

return ref;
return ref && !!ref[internals.symbol];
}
};


exports.isRef = function (ref) {
internals.ancestor = function (key, separator) {

return typeof ref === 'function' && ref.isJoi;
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.push = function (array, ref) {
internals.display = function (key, separator, ancestor) {

if (!ancestor) {
return `ref:${separator}${key}`;
}

if (ancestor === 1) {
return `ref:${key}`;
}

return `ref:${new Array(3).fill('.').join('')}${key}`;
};


internals.Ref.toSibling = 0;
internals.Ref.toParent = 1;


internals.Ref.Manager = class {

constructor() {

this.refs = []; // 0: [self refs], 1: [parent refs], 2: [grandparent refs], ...
}

register(source, target = internals.Ref.toParent) {

if (!source) {
return;
}

// Any

if (Utils.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 (internals.Ref.isRef(source) &&
!source.isContext &&
source.ancestor - target >= 0) {

this.refs.push({ ancestor: source.ancestor - target, root: source.root });
}
}

clone() {

const copy = new internals.Ref.Manager();
copy.refs = Hoek.clone(this.refs);
return copy;
}

if (exports.isRef(ref) &&
!ref.isContext) {
roots() {

array.push(ref.root);
return this.refs.filter((ref) => !ref.ancestor).map((ref) => ref.root);
}
};
4 changes: 3 additions & 1 deletion lib/settings.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const Hoek = require('@hapi/hoek');
const Marker = require('@hapi/marker');


const internals = {};
Expand All @@ -23,7 +24,8 @@ exports.defaults = {

exports.symbols = {
settingsCache: Symbol('settingsCache'),
schema: Symbol('schema')
schema: Symbol('schema'), // Used by describe() to include a reference to the schema
any: Marker('joi-any-base') // Used to internally identify any-based types (shared with other joi versions)
};


Expand Down
Loading

0 comments on commit 7e86c05

Please sign in to comment.