Skip to content

Commit

Permalink
Merge pull request #1659 from erquhart/assert-item
Browse files Browse the repository at this point in the history
Add array().assertItem(). Closes #1656
  • Loading branch information
Marsup committed Nov 24, 2018
2 parents 99e505d + c21e7d4 commit 354f6fa
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 0 deletions.
50 changes: 50 additions & 0 deletions API.md
Expand Up @@ -57,6 +57,7 @@
- [`array.max(limit)`](#arraymaxlimit)
- [`array.length(limit)`](#arraylengthlimit)
- [`array.unique([comparator], [options])`](#arrayuniquecomparator-options)
- [`array.assertItem(schema)`](#arrayassertitemschema)
- [`boolean` - inherits from `Any`](#boolean---inherits-from-any)
- [`boolean.truthy(value)`](#booleantruthyvalue)
- [`boolean.falsy(value)`](#booleanfalsyvalue)
Expand Down Expand Up @@ -170,6 +171,8 @@
- [`array.ref`](#arrayref)
- [`array.sparse`](#arraysparse)
- [`array.unique`](#arrayunique)
- [`array.assertItemKnown`](#arrayassertitemknown)
- [`array.assertItemUnknown`](#arrayassertitemunknown)
- [`binary.base`](#binarybase)
- [`binary.length`](#binarylength)
- [`binary.max`](#binarymax)
Expand Down Expand Up @@ -1239,6 +1242,24 @@ schema.validate([{}, {}]);

💥 Possible validation errors:[`array.unique`](#arrayunique)

#### `array.assertItem(schema)`

Verifies that an assertion passes for at least one item in the array, where:
- `schema` - the validation rules required to satisfy the assertion. If the `schema` includes references, they are resolved against
the array item being tested, not the value of the `ref` target.

```js
const schema = Joi.array().items(
Joi.object({
a: Joi.string(),
b: Joi.number()
})
).assertItem(Joi.object({ a: Joi.string().valid('a'), b: Joi.number() }))
```

💥 Possible validation errors:[`array.assertItemKnown`](#arrayassertitemknown), [`array.assertitemUnknown`](#arrayassertitemunknown)


### `boolean` - inherits from `Any`

Generates a schema object that matches a boolean data type. Can also be called via `bool()`. If the validation `convert`
Expand Down Expand Up @@ -3008,6 +3029,35 @@ A duplicate value was found in an array.
}
```

#### `array.assertItemKnown`

**Description**

The schema on an [`array.assertItem()`](#arrayassertitem) failed to validate. This error happens when the schema is labelled.

**Context**
```ts
{
key: string, // Last element of the path accessing the value, `undefined` if at the root
label: string, // Label if defined, otherwise it's the key
assertionLabel: string // Label of assertion schema
}
```

#### `array.assertItemUnknown`

**Description**

The schema on an [`array.assertItem()`](#arrayassertitem) failed to validate. This error happens when the schema is unlabelled.

**Context**
```ts
{
key: string, // Last element of the path accessing the value, `undefined` if at the root
label: string, // Label if defined, otherwise it's the key
}
```

#### `binary.base`

**Description**
Expand Down
2 changes: 2 additions & 0 deletions lib/language.js
Expand Up @@ -37,6 +37,8 @@ exports.errors = {
includesRequiredBoth: 'does not contain {{knownMisses}} and {{unknownMisses}} other required value(s)',
excludes: 'at position {{pos}} contains an excluded value',
excludesSingle: 'single value of "{{!label}}" contains an excluded value',
assertItemKnown: 'does not contain a match for type "{{!assertionLabel}}"',
assertItemUnknown: 'failed an assertion test',
min: 'must contain at least {{limit}} items',
max: 'must contain less than or equal to {{limit}} items',
length: 'must contain {{limit}} items',
Expand Down
35 changes: 35 additions & 0 deletions lib/types/array/index.js
Expand Up @@ -483,6 +483,41 @@ internals.Array = class extends Any {
});
}

assertItem(schema) {

try {
schema = Cast.schema(this._currentJoi, schema);
}
catch (castErr) {
if (castErr.hasOwnProperty('path')) {
castErr.message = castErr.message + '(' + castErr.path + ')';
}

throw castErr;
}

return this._test('assertItem', schema, function (value, state, options) {

const isValid = value.some((item, idx) => {

const localState = new State(idx, [...state.path, idx], state.key, state.reference);
const result = schema._validate(item, localState, options);
return !result.errors;
});

if (isValid) {
return value;
}

const assertionLabel = schema._getLabel();
if (assertionLabel) {
return this.createError('array.assertItemKnown', { assertionLabel }, state, options);
}

return this.createError('array.assertItemUnknown', null, state, options);
});
}

unique(comparator, configs) {

Hoek.assert(comparator === undefined ||
Expand Down
158 changes: 158 additions & 0 deletions test/types/array.js
Expand Up @@ -762,6 +762,164 @@ describe('array', () => {
});
});

describe('assertItem()', () => {

it('shows path to errors in schema', () => {

expect(() => {

Joi.array().assertItem({
a: {
b: {
c: {
d: undefined
}
}
}
});
}).to.throw(Error, 'Invalid schema content: (a.b.c.d)');
});

it('shows errors in schema', () => {

expect(() => {

Joi.array().assertItem(undefined);
}).to.throw(Error, 'Invalid schema content: ');
});

it('works with object.assert', () => {

const schema = Joi.array().items(
Joi.object().keys({
a: {
b: Joi.string(),
c: Joi.number()
},
d: {
e: Joi.any()
}
})
).assertItem(Joi.object().assert('d.e', Joi.ref('a.c'), 'equal to a.c'));

Helper.validate(schema, [
[[{ a: { b: 'x', c: 5 }, d: { e: 5 } }], true]
]);
});


it('does not throw if assertion passes', () => {

const schema = Joi.array().assertItem(Joi.string());
Helper.validate(schema, [
[['foo'], true]
]);
});

it('throws with proper message if assertion fails on unknown schema', () => {

const schema = Joi.array().assertItem(Joi.string());
Helper.validate(schema, [
[[0], false, null, {
message: '"value" failed an assertion test',
details: [{
message: '"value" failed an assertion test',
path: [],
type: 'array.assertItemUnknown',
context: { label: 'value', key: undefined }
}]
}]
]);
});

it('throws with proper message if assertion fails on known schema', () => {

const schema = Joi.array().assertItem(Joi.string().label('foo'));
Helper.validate(schema, [
[[0], false, null, {
message: '"value" does not contain a match for type "foo"',
details: [{
message: '"value" does not contain a match for type "foo"',
path: [],
type: 'array.assertItemKnown',
context: { label: 'value', key: undefined, assertionLabel: 'foo' }
}]
}]
]);
});

it('shows correct path for error', () => {

const schema = Joi.object({
arr: Joi.array().assertItem(Joi.string())
});
Helper.validate(schema, [
[{ arr: [0] }, false, null, {
message: 'child "arr" fails because ["arr" failed an assertion test]',
details: [{
message: '"arr" failed an assertion test',
path: ['arr'],
type: 'array.assertItemUnknown',
context: { label: 'arr', key: 'arr' }
}]
}]
]);
});

it('supports nested arrays', () => {

const schema = Joi.object({
arr: Joi.array().items(
Joi.object({ foo: Joi.array().assertItem(Joi.string()) })
)
});
Helper.validate(schema, [
[{ arr: [{ foo: ['bar'] }] }, true]
]);
});

it('provides accurate error message for nested arrays', () => {

const schema = Joi.object({
arr: Joi.array().items(
Joi.object({ foo: Joi.array().assertItem(Joi.string()) })
)
});
Helper.validate(schema, [
[{ arr: [{ foo: [0] }] }, false, null, {
message: 'child "arr" fails because ["arr" at position 0 fails because [child "foo" fails because ["foo" failed an assertion test]]]',
details: [{
message: '"foo" failed an assertion test',
path: ['arr', 0, 'foo'],
type: 'array.assertItemUnknown',
context: { label: 'foo', key: 'foo' }
}]
}]
]);
});

it('handles multiple assertions', () => {

const schema = Joi.array().assertItem(Joi.string()).assertItem(Joi.number());
Helper.validate(schema, [
[['foo', 0], true]
]);

Helper.validate(schema, [
[['foo'], false, null, {
message: '"value" failed an assertion test',
details: [{
message: '"value" failed an assertion test',
path: [],
type: 'array.assertItemUnknown',
context: { label: 'value', key: undefined }
}]
}]
]);
});
});


describe('validate()', () => {

it('should, by default, allow undefined, allow empty array', () => {
Expand Down

0 comments on commit 354f6fa

Please sign in to comment.