Skip to content

Commit

Permalink
Fix handling of shadow values. Closes #2156
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse committed Oct 2, 2019
1 parent 5d6d5ed commit 075c6bb
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 94 deletions.
6 changes: 5 additions & 1 deletion lib/base.js
Expand Up @@ -657,7 +657,11 @@ internals.Base = class {
prefs.abortEarly = true;
prefs._externals = false;

return !Validator.validate(value, this, state, prefs, overrides).errors;
state.snapshot();
const result = !Validator.validate(value, this, state, prefs, overrides).errors;
state.restore();

return result;
}

$_modify(options) {
Expand Down
45 changes: 0 additions & 45 deletions lib/common.js
Expand Up @@ -163,51 +163,6 @@ exports.preferences = function (target, source) {
};


exports.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 exports.State(path, ancestors, this);

if (schema &&
state.schemas) {

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

return state;
}

nest(schema, debug) {

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


internals.schemas = function (schema) {

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

return schema;
};


exports.tryWithPath = function (fn, key, options = {}) {

try {
Expand Down
7 changes: 6 additions & 1 deletion lib/ref.js
Expand Up @@ -149,7 +149,7 @@ internals.Ref = class {
state.mainstay.shadow &&
options.shadow !== false) {

resolved = state.mainstay.shadow.get(this.path);
resolved = state.mainstay.shadow.get(this.absolute(state));
}

if (resolved === undefined) {
Expand Down Expand Up @@ -179,6 +179,11 @@ internals.Ref = class {
return this.display;
}

absolute(state) {

return [...state.path.slice(0, -this.ancestor), ...this.path];
}

clone() {

return new internals.Ref(this);
Expand Down
150 changes: 150 additions & 0 deletions lib/state.js
@@ -0,0 +1,150 @@
'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));
}
}

restore() {

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


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);
}
else {
parent.delete(own);
}
}
};
14 changes: 12 additions & 2 deletions lib/types/alternatives.js
Expand Up @@ -49,11 +49,17 @@ module.exports = Any.extend({

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

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

if (!hits) {
Expand All @@ -76,11 +82,15 @@ module.exports = Any.extend({
// Try

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

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

localState.restore();
errors.push({ schema: item.schema, reports: result.errors });
continue;
}
Expand Down
21 changes: 17 additions & 4 deletions lib/types/array.js
Expand Up @@ -80,7 +80,7 @@ module.exports = Any.extend({
obj.$_mutateRegister(schema);
return obj;
},
validate(value, { state, prefs, error, schema }, { schema: has }) {
validate(value, { state, prefs, error }, { schema: has }) {

const ancestors = [value, ...state.ancestors];
for (let i = 0; i < value.length; ++i) {
Expand Down Expand Up @@ -219,7 +219,10 @@ module.exports = Any.extend({
const requiredChecks = [];
let jl = requireds.length;
for (let j = 0; j < jl; ++j) {
const res = requireds[j].$_validate(item, state.localize(path, ancestors, requireds[j]), prefs);
const localState = state.localize(path, ancestors, requireds[j]);
localState.snapshot();

const res = requireds[j].$_validate(item, localState, prefs);
requiredChecks[j] = res;

if (!res.errors) {
Expand All @@ -240,6 +243,8 @@ module.exports = Any.extend({

break;
}

localState.restore();
}

if (isValid) {
Expand All @@ -261,7 +266,10 @@ module.exports = Any.extend({
res = requiredChecks[previousCheck];
}
else {
res = inclusion.$_validate(item, state.localize(path, ancestors, inclusion), prefs);
const localState = state.localize(path, ancestors, inclusion);
localState.snapshot();

res = inclusion.$_validate(item, localState, prefs);
if (!res.errors) {
if (inclusion._flags.result === 'strip') {
internals.fastSplice(value, i);
Expand All @@ -281,6 +289,8 @@ module.exports = Any.extend({
isValid = true;
break;
}

localState.restore();
}

// Return the actual error if only one inclusion defined
Expand Down Expand Up @@ -308,7 +318,9 @@ module.exports = Any.extend({
continue;
}

if (schema.$_terms._inclusions.length && !isValid) {
if (schema.$_terms._inclusions.length &&
!isValid) {

if (stripUnknown) {
internals.fastSplice(value, i);
--i;
Expand All @@ -333,6 +345,7 @@ module.exports = Any.extend({

return errors.length ? errors : value;
},

priority: true,
manifest: false
},
Expand Down
3 changes: 2 additions & 1 deletion lib/types/keys.js
Expand Up @@ -202,7 +202,8 @@ module.exports = Any.extend({
validate(value, { error, prefs, state }, { subject, schema, message }) {

const about = subject.resolve(value, state, prefs);
if (schema.$_match(about, state.localize([], [value, ...state.ancestors], schema), prefs)) {
const path = Ref.isRef(subject) ? subject.absolute(state) : [];
if (schema.$_match(about, state.localize(path, [value, ...state.ancestors], schema), prefs)) {
return value;
}

Expand Down

0 comments on commit 075c6bb

Please sign in to comment.