Skip to content

Commit

Permalink
Add a "peek" feature to alternatives.when
Browse files Browse the repository at this point in the history
  • Loading branch information
Marsup committed Oct 14, 2017
1 parent b064737 commit 1081aed
Show file tree
Hide file tree
Showing 5 changed files with 715 additions and 475 deletions.
45 changes: 38 additions & 7 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -685,12 +685,12 @@ const b = Joi.string().valid('b');
const ab = a.concat(b);
```

#### `any.when(ref, options)`
#### `any.when(condition, options)`

Converts the type into an [`alternatives`](#alternatives) type where the conditions are merged into the type definition where:
- `ref` - the key name or [reference](#refkey-options).
- `condition` - the key name or [reference](#refkey-options), or a schema.
- `options` - an object with:
- `is` - the required condition **joi** type. Anything that is not a joi schema will be converted using [Joi.compile](#compileschema).
- `is` - the required condition **joi** type. Anything that is not a joi schema will be converted using [Joi.compile](#compileschema). Forbidden when `condition` is a schema.
- `then` - the alternative schema type if the condition is true. Required if `otherwise` is missing.
- `otherwise` - the alternative schema type if the condition is false. Required if `then` is missing.

Expand All @@ -704,6 +704,23 @@ const schema = {
};
```

Or with a schema:
```js
const schema = Joi.object({
a: Joi.any().valid('x'),
b: Joi.any()
}).when(Joi.object({ b: Joi.exist() }).unknown(), {
then: Joi.object({
a: Joi.valid('y')
}),
otherwise: Joi.object({
a: Joi.valid('z')
})
});
```

Note that this style is much more useful when your whole schema depends on the value of one of its property, or if you find yourself repeating the check for many keys of an object.

Alternatively, if you want to specify a specific type such as `string`, `array`, etc, you can do so like this:

```js
Expand Down Expand Up @@ -2018,12 +2035,13 @@ const alt = Joi.alternatives().try(Joi.number(), Joi.string());
alt.validate('a', (err, value) => { });
```

#### `alternatives.when(ref, options)`
#### `alternatives.when(condition, options)`

Adds a conditional alternative schema type based on another key (not the same as `any.when()`) value where:
- `ref` - the key name or [reference](#refkey-options).
Adds a conditional alternative schema type, either based on another key (not the same as `any.when()`) value, or a
schema peeking into the current value, where:
- `condition` - the key name or [reference](#refkey-options), or a schema.
- `options` - an object with:
- `is` - the required condition **joi** type.
- `is` - the required condition **joi** type. Forbidden when `condition` is a schema.
- `then` - the alternative schema type to **try** if the condition is true. Required if `otherwise` is missing.
- `otherwise` - the alternative schema type to **try** if the condition is false. Required if `then` is missing.

Expand All @@ -2034,6 +2052,19 @@ const schema = {
};
```

```js
const schema = Joi.alternatives().when(Joi.object({ b: 5 }).unknown(), {
then: Joi.object({
a: Joi.string(),
b: Joi.any()
}),
otherwise: Joi.object({
a: Joi.number(),
b: Joi.any()
})
});
```

Note that `when()` only adds additional alternatives to try and does not impact the overall type. Setting
a `required()` rule on a single alternative will not apply to the overall key. For example,
this definition of `a`:
Expand Down
45 changes: 30 additions & 15 deletions lib/types/alternatives/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ internals.Alternatives = class extends Any {

for (let i = 0; i < il; ++i) {
const item = this._inner.matches[i];
const schema = item.schema;
if (!schema) {
const failed = item.is._validate(item.ref(state.reference || state.parent, options), null, options, state.parent).errors;
if (!item.schema) {
const schema = item.peek || item.is;
const input = item.is ? item.ref(state.reference || state.parent, options) : value;
const failed = schema._validate(input, null, options, state.parent).errors;

if (failed) {
if (item.otherwise) {
Expand All @@ -51,7 +52,7 @@ internals.Alternatives = class extends Any {
continue;
}

const result = schema._validate(value, state, options);
const result = item.schema._validate(value, state, options);
if (!result.errors) { // Found a valid match
return result;
}
Expand Down Expand Up @@ -84,25 +85,35 @@ internals.Alternatives = class extends Any {
return obj;
}

when(ref, options) {
when(condition, options) {

Hoek.assert(Ref.isRef(ref) || typeof ref === 'string', 'Invalid reference:', ref);
let schemaCondition = false;
Hoek.assert(Ref.isRef(condition) || typeof condition === 'string' || (schemaCondition = condition instanceof Any), 'Invalid condition:', condition);
Hoek.assert(options, 'Missing options');
Hoek.assert(typeof options === 'object', 'Invalid options');
Hoek.assert(options.hasOwnProperty('is'), 'Missing "is" directive');
if (schemaCondition) {
Hoek.assert(!options.hasOwnProperty('is'), '"is" can not be used with a schema condition');
}
else {
Hoek.assert(options.hasOwnProperty('is'), 'Missing "is" directive');
}
Hoek.assert(options.then !== undefined || options.otherwise !== undefined, 'options must have at least one of "then" or "otherwise"');

const obj = this.clone();
let is = Cast.schema(this._currentJoi, options.is);
let is;
if (!schemaCondition) {
is = Cast.schema(this._currentJoi, options.is);

if (options.is === null || !(Ref.isRef(options.is) || options.is instanceof Any)) {
if (options.is === null || !(Ref.isRef(options.is) || options.is instanceof Any)) {

// Only apply required if this wasn't already a schema or a ref, we'll suppose people know what they're doing
is = is.required();
// Only apply required if this wasn't already a schema or a ref, we'll suppose people know what they're doing
is = is.required();
}
}

const item = {
ref: Cast.ref(ref),
ref: schemaCondition ? null : Cast.ref(condition),
peek: schemaCondition ? condition : null,
is,
then: options.then !== undefined ? Cast.schema(this._currentJoi, options.then) : undefined,
otherwise: options.otherwise !== undefined ? Cast.schema(this._currentJoi, options.otherwise) : undefined
Expand All @@ -114,8 +125,10 @@ internals.Alternatives = class extends Any {
item.otherwise = item.otherwise && obj._baseType.concat(item.otherwise);
}

Ref.push(obj._refs, item.ref);
obj._refs = obj._refs.concat(item.is._refs);
if (!schemaCondition) {
Ref.push(obj._refs, item.ref);
obj._refs = obj._refs.concat(item.is._refs);
}

if (item.then && item.then._refs) {
obj._refs = obj._refs.concat(item.then._refs);
Expand Down Expand Up @@ -146,9 +159,11 @@ internals.Alternatives = class extends Any {

// when()

const when = {
const when = item.is ? {
ref: item.ref.toString(),
is: item.is.describe()
} : {
peek: item.peek.describe()
};

if (item.then) {
Expand Down
9 changes: 7 additions & 2 deletions lib/types/any/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ module.exports = internals.Any = class {
return obj;
}

when(ref, options) {
when(condition, options) {

Hoek.assert(options && typeof options === 'object', 'Invalid options');
Hoek.assert(options.then !== undefined || options.otherwise !== undefined, 'options must have at least one of "then" or "otherwise"');
Expand All @@ -394,7 +394,12 @@ module.exports = internals.Any = class {
const otherwise = options.hasOwnProperty('otherwise') ? this.concat(Cast.schema(this._currentJoi, options.otherwise)) : undefined;

Alternatives = Alternatives || require('../alternatives');
const obj = Alternatives.when(ref, { is: options.is, then, otherwise });

const alternativeOptions = { then, otherwise };
if (Object.prototype.hasOwnProperty.call(options, 'is')) {
alternativeOptions.is = options.is;
}
const obj = Alternatives.when(condition, alternativeOptions);
obj._flags.presence = 'ignore';
obj._baseType = this;

Expand Down
Loading

0 comments on commit 1081aed

Please sign in to comment.