Skip to content

Commit

Permalink
Alterations. Closes 1954
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse committed Jun 30, 2019
1 parent 6eb66f6 commit f85705f
Show file tree
Hide file tree
Showing 9 changed files with 3,473 additions and 2,990 deletions.
42 changes: 42 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- [`any`](#any)
- [`any.type`](#anytype)
- [`any.allow(...values)`](#anyallowvalues)
- [`any.alter(targets)`](#anyaltertargets)
- [`any.cast(to)`](#anycastto)
- [`any.concat(schema)`](#anyconcatschema)
- [`any.default([value, [description]])`](#anydefaultvalue-description)
Expand All @@ -29,6 +30,7 @@
- [`any.empty(schema)`](#anyemptyschema)
- [`any.error(err)`](#anyerrorerr)
- [`any.example(...values)`](#anyexamplevalues)
- [`any.external(method)`](#anyexternalmethod)
- [`any.extract(path)`](#anyextractpath)
- [`any.failover([value, [description]])`](#anyfailovervalue-description)
- [`any.forbidden()`](#anyforbidden)
Expand All @@ -49,6 +51,7 @@
- [`any.strict(isStrict)`](#anystrictisstrict)
- [`any.strip()`](#anystrip)
- [`any.tags(tags)`](#anytagstags)
- [`any.tailor(targets)`](#anytailortargets)
- [`any.unit(name)`](#anyunitname)
- [`any.valid(...values)` - aliases: `only`, `equal`](#anyvalidvalues---aliases-only-equal)
- [`any.validate(value, [options])`](#anyvalidatevalue-options)
Expand Down Expand Up @@ -585,6 +588,26 @@ const schema = {
};
```

#### `any.alter(targets)`

Assign target alteration options to a schema that are applied when [`any.tailor()`](#anytailortargets)
is called where:
- `targets` - an object where each key is a target name, and each value is a function with signature
`function(schema)` that returns a schema.

```js
const schema = Joi.object({
key: Joi.string()
.alter({
get: (schema) => schema.required(),
post: (schema) => schema.forbidden()
})
});

const getSchema = schema.tailor('get');
const postSchema = schema.tailor('post');
```

#### `any.cast(to)`

Casts the validated value to the specified type where:
Expand Down Expand Up @@ -989,6 +1012,25 @@ Annotates the key where:
const schema = Joi.any().tags(['api', 'user']);
```

#### `any.tailor(targets)`

Applies any assigned target alterations to a copy of the schema that were applied via
[`any.alter()`](#anyaltertargets) where:
- `targets` - a single target string or array or target strings to apply.

```js
const schema = Joi.object({
key: Joi.string()
.alter({
get: (schema) => schema.required(),
post: (schema) => schema.forbidden()
})
});

const getSchema = schema.tailor('get');
const postSchema = schema.tailor(['post']);
```

#### `any.unit(name)`

Annotates the key where:
Expand Down
4 changes: 4 additions & 0 deletions lib/about.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ exports.describe = function (schema) {
});
}

if (schema._inner.alterations) {
description.alterations = schema._inner.alterations.slice();
}

if (schema._inner.externals) {
description.externals = schema._inner.externals.slice();
}
Expand Down
20 changes: 20 additions & 0 deletions lib/types/alternatives.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,26 @@ internals.Alternatives = class extends Any {
return obj;
}

tailor(targets) {

let obj = super.tailor(targets);
if (obj === this) {
obj = this.clone();
}

for (let i = 0; i < obj._inner.matches.length; ++i) {
const match = Object.assign({}, obj._inner.matches[i]);
obj._inner.matches[i] = match;
for (const key of ['schema', 'peek', 'is', 'then', 'otherwise']) {
if (match[key]) {
match[key] = match[key].tailor(targets);
}
}
}

return obj._rebuild();
}

try(schemas) {

Hoek.assert(schemas, 'Missing alternative schemas');
Expand Down
40 changes: 40 additions & 0 deletions lib/types/any.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ module.exports = internals.Any = class {
this._meta = [];

this._inner = { // Hash of arrays of immutable objects (extended by other types)
alterations: null,
externals: null
};
}
Expand Down Expand Up @@ -129,6 +130,23 @@ module.exports = internals.Any = class {
return obj;
}

alter(targets) {

Hoek.assert(targets && typeof targets === 'object' && !Array.isArray(targets), 'Invalid targets argument');
Hoek.assert(!this._inRuleset(), 'Cannot set alterations inside a ruleset');

const obj = this.clone();
obj._inner.alterations = obj._inner.alterations || [];
for (const target in targets) {
const adjuster = targets[target];
Hoek.assert(typeof adjuster === 'function', 'Alteration adjuster for', target, 'must be a function');
obj._inner.alterations.push({ target, adjuster });
}

obj._ruleset = false;
return obj;
}

cast(to) {

Hoek.assert(to === false || this._casts[to], 'Type', this._type, 'does not support casting to', to);
Expand Down Expand Up @@ -589,6 +607,28 @@ module.exports = internals.Any = class {
return this._ids.labels(path);
}

tailor(targets) {

Hoek.assert(!this._inRuleset(), 'Cannot tailor inside a ruleset');

if (!this._inner.alterations) {
return this;
}

targets = [].concat(targets);

let obj = this; // eslint-disable-line consistent-this
for (const { target, adjuster } of this._inner.alterations) {
if (targets.includes(target)) {
obj = adjuster(obj);
Hoek.assert(Common.isSchema(obj), 'Alteration adjuster for', target, 'failed to return a schema object');
}
}

obj._ruleset = false;
return obj;
}

validate(value, options) {

return Validator.entry(value, this, options);
Expand Down
79 changes: 75 additions & 4 deletions lib/types/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,45 @@ internals.Object = class extends Any {

_override(id, schema) {

for (const child of this._inner.children) {
const childId = child.schema._flags.id || child.key;
if (id === childId) {
return this.keys({ [child.key]: schema });
if (this._inner.children) {
for (const child of this._inner.children) {
const childId = child.schema._flags.id || child.key;
if (id === childId) {
return this.keys({ [child.key]: schema });
}
}
}

if (this._inner.patterns) {
for (let i = 0; i < this._inner.patterns.length; ++i) {
const pattern = this._inner.patterns[i];
for (const key of ['schema', 'rule', 'matches']) {
if (pattern[key] &&
id === pattern[key]._flags.id) {

const obj = this.clone();
obj._inner.patterns[i] = Object.assign({}, pattern, { [key]: schema });
return obj._rebuild();
}
}
}
}

let i = 0;
for (const test of this._tests) {
if (test.name === 'assert' &&
id === test.rule._options.args.schema._flags.id) {

const obj = this.clone();
const clone = Hoek.clone(test);
clone.rule._options.args.schema = schema;
clone.arg = clone.rule._options.args;
obj._tests[i] = clone;
return obj._rebuild();
}

++i;
}
}

// About
Expand Down Expand Up @@ -531,6 +564,44 @@ internals.Object = class extends Any {
return this._rule('schema', { args: { type } });
}

tailor(targets) {

let obj = super.tailor(targets);
if (obj === this) {
obj = this.clone();
}

if (obj._inner.children) {
for (let i = 0; i < obj._inner.children.length; ++i) {
const child = obj._inner.children[i];
obj._inner.children[i] = Object.assign({}, child, { schema: child.schema.tailor(targets) });
}
}

if (obj._inner.patterns) {
for (let i = 0; i < obj._inner.patterns.length; ++i) {
const pattern = obj._inner.patterns[i];
for (const key of ['schema', 'rule', 'matches']) {
if (pattern[key]) {
obj._inner.patterns[i] = Object.assign({}, pattern, { [key]: pattern[key].tailor(targets) });
}
}
}
}

for (let i = 0; i < obj._tests.length; ++i) {
const test = obj._tests[i];
if (test.name === 'assert') {
const clone = Hoek.clone(test);
clone.rule._options.args.schema = clone.rule._options.args.schema.tailor(targets);
clone.arg = clone.rule._options.args;
obj._tests[i] = clone;
}
}

return obj._rebuild();
}

unknown(allow) {

return this._flag('allowUnknown', allow !== false);
Expand Down
73 changes: 73 additions & 0 deletions test/modify.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,79 @@ describe('Modify', () => {
}]
]);
});

it('adjusts object assert', () => {

const before = Joi.object({
a: Joi.object({
b: Joi.number()
}),
b: Joi.object({
d: Joi.number()
})
})
.assert('b.d', Joi.valid(1))
.assert('a.b', Joi.valid(Joi.ref('b.d')).id('assert'));

const after = Joi.object({
a: Joi.object({
b: Joi.number()
}),
b: Joi.object({
d: Joi.number()
})
})
.assert('b.d', Joi.valid(1))
.assert('a.b', Joi.valid(Joi.ref('b.d'), 'x').id('assert'));

after._ruleset = false;

expect(before.fork('assert', (schema) => schema.valid('x'))).to.equal(after);
});

it('adjusts object assert (with pattern)', () => {

const before = Joi.object({
a: Joi.object({
b: Joi.number()
}),
b: Joi.object({
d: Joi.number()
})
})
.min(2)
.pattern(/\d/, Joi.any())
.assert('a.b', Joi.valid(Joi.ref('b.d')).id('assert'));

const after = Joi.object({
a: Joi.object({
b: Joi.number()
}),
b: Joi.object({
d: Joi.number()
})
})
.min(2)
.pattern(/\d/, Joi.any())
.assert('a.b', Joi.valid(Joi.ref('b.d'), 'x').id('assert'));

after._ruleset = false;

expect(before.fork('assert', (schema) => schema.valid('x')).describe()).to.equal(after.describe());
});

it('adjusts object pattern', () => {

const before = Joi.object()
.pattern(/.*/, Joi.valid('x').id('pattern'));

const after = Joi.object()
.pattern(/.*/, Joi.valid('x', 'y').id('pattern'));

after._ruleset = false;

expect(before.fork('pattern', (schema) => schema.valid('y'))).to.equal(after);
});
});

describe('array', () => {
Expand Down
Loading

0 comments on commit f85705f

Please sign in to comment.