Skip to content

Commit

Permalink
feat: support self references (#443)
Browse files Browse the repository at this point in the history
`key` cannot be a function since [`validateName`](https://github.com/jquense/yup/blob/d02ff5e59e004b4c5189d1b9fc0055cff45c61df/src/Reference.js#L3) ensures that it is a string.

`isSelf` is not used anywhere and `key="."` isn't supported by `property-expr` getter.

For working self references i opened [pull request](jquense/expr#9) in `property-expr` to support empty string path. (currently `getter("")` works, but `getter("", true)` which is used in Reference.js throws)
  • Loading branch information
vonagam authored and jquense committed Feb 7, 2019
1 parent 3047b33 commit 1cac515
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 46 deletions.
8 changes: 2 additions & 6 deletions src/Condition.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,9 @@ class Conditional {
}
}

getValue(parent, context) {
let values = this.refs.map(r => r.getValue(parent, context));
resolve(ctx, options) {
let values = this.refs.map(ref => ref.getValue(options));

return values;
}

resolve(ctx, values) {
let schema = this.fn.apply(ctx, values.concat(ctx));

if (schema !== undefined && !isSchema(schema))
Expand Down
75 changes: 48 additions & 27 deletions src/Reference.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,69 @@
import { getter } from 'property-expr';

let validateName = d => {
if (typeof d !== 'string')
throw new TypeError("ref's must be strings, got: " + d);
const prefixes = {
context: '$',
value: '.',
};

export default class Reference {
static isRef(value) {
return !!(value && (value.__isYupRef || value instanceof Reference));
}
constructor(key, options = {}) {
if (typeof key !== 'string')
throw new TypeError('ref must be a string, got: ' + key);

toString() {
return `Ref(${this.key})`;
this.key = key.trim();

if (key === '') throw new TypeError('ref must be a non-empty string');

this.isContext = this.key[0] === prefixes.context;
this.isValue = this.key[0] === prefixes.value;
this.isSibling = !this.isContext && !this.isValue;

let prefix = this.isContext
? prefixes.context
: this.isValue
? prefixes.value
: '';

this.path = this.key.slice(prefix.length);
this.getter = this.path && getter(this.path, true);
this.map = options.map;
}

constructor(key, mapFn, options = {}) {
validateName(key);
let prefix = options.contextPrefix || '$';
getValue(options) {
let result = this.isContext
? options.context
: this.isValue
? options.value
: options.parent;

if (typeof key === 'function') {
key = '.';
}
if (this.getter) result = this.getter(result || {});

this.key = key.trim();
this.prefix = prefix;
this.isContext = this.key.indexOf(prefix) === 0;
this.isSelf = this.key === '.';
if (this.map) result = this.map(result);

return result;
}

this.path = this.isContext ? this.key.slice(this.prefix.length) : this.key;
this._get = getter(this.path, true);
this.map = mapFn || (value => value);
cast(value, options) {
return this.getValue({ ...options, value });
}

resolve() {
return this;
}

cast(value, { parent, context }) {
return this.getValue(parent, context);
describe() {
return {
type: 'ref',
key: this.key,
};
}

getValue(parent, context) {
let isContext = this.isContext;
let value = this._get(isContext ? context : parent || context || {});
return this.map(value);
toString() {
return `Ref(${this.key})`;
}

static isRef(value) {
return value && value.__isYupRef;
}
}

Expand Down
20 changes: 12 additions & 8 deletions src/mixed.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,10 @@ const proto = (SchemaType.prototype = {
return !this._typeCheck || this._typeCheck(v);
},

resolve({ context, parent }) {
resolve(options) {
if (this._conditions.length) {
return this._conditions.reduce(
(schema, match) =>
match.resolve(schema, match.getValue(parent, context)),
(schema, condition) => condition.resolve(schema, options),
this,
);
}
Expand All @@ -147,7 +146,7 @@ const proto = (SchemaType.prototype = {
},

cast(value, options = {}) {
let resolvedSchema = this.resolve(options);
let resolvedSchema = this.resolve({ ...options, value });
let result = resolvedSchema._cast(value, options);

if (
Expand Down Expand Up @@ -240,12 +239,12 @@ const proto = (SchemaType.prototype = {
},

validate(value, options = {}) {
let schema = this.resolve(options);
let schema = this.resolve({ ...options, value });
return schema._validate(value, options);
},

validateSync(value, options = {}) {
let schema = this.resolve(options);
let schema = this.resolve({ ...options, value });
let result, err;

schema
Expand All @@ -268,7 +267,7 @@ const proto = (SchemaType.prototype = {

isValidSync(value, options) {
try {
this.validateSync(value, { ...options });
this.validateSync(value, options);
return true;
} catch (err) {
if (err.name === 'ValidationError') return false;
Expand Down Expand Up @@ -380,11 +379,16 @@ const proto = (SchemaType.prototype = {
},

when(keys, options) {
if (arguments.length === 1) {
options = keys;
keys = '.';
}

var next = this.clone(),
deps = [].concat(keys).map(key => new Ref(key));

deps.forEach(dep => {
if (!dep.isContext) next._deps.push(dep.key);
if (dep.isSibling) next._deps.push(dep.key);
});

next._conditions.push(new Condition(deps, options));
Expand Down
6 changes: 4 additions & 2 deletions src/util/createValidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@ export default function createValidation(options) {
...rest
}) {
let parent = options.parent;
let resolve = value =>
Ref.isRef(value) ? value.getValue(parent, options.context) : value;
let resolve = item =>
Ref.isRef(item)
? item.getValue({ value, parent, context: options.context })
: item;

let createError = createErrorFactory({
message,
Expand Down
2 changes: 1 addition & 1 deletion src/util/sortFields.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default function sortFields(fields, excludes = []) {

if (!~nodes.indexOf(key)) nodes.push(key);

if (Ref.isRef(value) && !value.isContext) addNode(value.path, key);
if (Ref.isRef(value) && value.isSibling) addNode(value.path, key);
else if (isSchema(value) && value._deps)
value._deps.forEach(path => addNode(path, key));
}
Expand Down
34 changes: 32 additions & 2 deletions test/mixed.js
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ describe('Mixed Types ', () => {

it('should handle multiple conditionals', function() {
let called = false;
let inst = mixed().when(['prop', 'other'], function(prop, other) {
let inst = mixed().when(['$prop', '$other'], function(prop, other) {
other.should.equal(true);
prop.should.equal(1);
called = true;
Expand All @@ -665,7 +665,7 @@ describe('Mixed Types ', () => {
inst.cast({}, { context: { prop: 1, other: true } });
called.should.equal(true);

inst = mixed().when(['prop', 'other'], {
inst = mixed().when(['$prop', '$other'], {
is: 5,
then: mixed().required(),
});
Expand Down Expand Up @@ -720,6 +720,36 @@ describe('Mixed Types ', () => {
inst.default().should.eql({ prop: undefined });
});

it('should support self references in conditions', async function() {
let inst = number().when('.', {
is: value => value > 0,
then: number().min(5),
});

await inst
.validate(4)
.should.be.rejectedWith(ValidationError, /must be greater/);

await inst.validate(5).should.be.fulfilled();

await inst.validate(-1).should.be.fulfilled();
});

it('should support conditional single argument as options shortcut', async function() {
let inst = number().when({
is: value => value > 0,
then: number().min(5),
});

await inst
.validate(4)
.should.be.rejectedWith(ValidationError, /must be greater/);

await inst.validate(5).should.be.fulfilled();

await inst.validate(-1).should.be.fulfilled();
});

it('should use label in error message', async function() {
let label = 'Label';
let inst = object({
Expand Down

0 comments on commit 1cac515

Please sign in to comment.