Skip to content

Commit

Permalink
Merge pull request #1511 from WesTyler/unique_ignoreUndefined_#1498
Browse files Browse the repository at this point in the history
Add optional "config" param to array.unique for ignoreUndefined. Closes #1498
  • Loading branch information
Marsup committed Jun 9, 2018
2 parents f75f0d3 + 8071b8a commit 83eb8eb
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 5 deletions.
18 changes: 16 additions & 2 deletions API.md
Expand Up @@ -56,7 +56,7 @@
- [`array.min(limit)`](#arrayminlimit)
- [`array.max(limit)`](#arraymaxlimit)
- [`array.length(limit)`](#arraylengthlimit)
- [`array.unique([comparator])`](#arrayuniquecomparator)
- [`array.unique([comparator], [options])`](#arrayuniquecomparator-options)
- [`boolean` - inherits from `Any`](#boolean---inherits-from-any)
- [`boolean.truthy(value)`](#booleantruthyvalue)
- [`boolean.falsy(value)`](#booleanfalsyvalue)
Expand Down Expand Up @@ -1022,13 +1022,15 @@ const schema = Joi.object({
});
```

#### `array.unique([comparator])`
#### `array.unique([comparator], [options])`

Requires the array values to be unique.

You can provide a custom `comparator` that is either :
- a function that takes 2 parameters to compare. This function should return whether the 2 parameters are equal or not, you are also **responsible** for this function not to fail, any `Error` would bubble out of Joi.
- a string in dot notation representing the path of the element to do uniqueness check on. Any missing path will be considered undefined, and can as well only exist once.
You can also provide an `options` object containing:
- `ignoreUndefined`. When set to `true`, undefined values for the dot notation string comparator will not cause the array to fail on uniqueness.

Note: remember that if you provide a custom comparator function, different types can be passed as parameter depending on the rules you set on items.

Expand All @@ -1046,6 +1048,18 @@ const schema = Joi.array().unique((a, b) => a.property === b.property);
const schema = Joi.array().unique('customer.id');
```

```js
let schema = Joi.array().unique('identifier');

schema.validate([{}, {}]);
// ValidationError: "value" position 1 contains a duplicate value

schema = Joi.array().unique('identifier', { ignoreUndefined: true });

schema.validate([{}, {}]);
// error: null
```

### `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
13 changes: 10 additions & 3 deletions lib/types/array/index.js
Expand Up @@ -477,13 +477,19 @@ internals.Array = class extends Any {
});
}

unique(comparator) {
unique(comparator, configs) {

Hoek.assert(comparator === undefined ||
typeof comparator === 'function' ||
typeof comparator === 'string', 'comparator must be a function or a string');

const settings = {};
Hoek.assert(configs === undefined ||
typeof configs === 'object', 'configs must be an object');

const settings = {
ignoreUndefined: (configs && configs.ignoreUndefined) || false
};


if (typeof comparator === 'string') {
settings.path = comparator;
Expand All @@ -505,6 +511,7 @@ internals.Array = class extends Any {
};

const compare = settings.comparator || Hoek.deepEqual;
const ignoreUndefined = settings.ignoreUndefined;

for (let i = 0; i < value.length; ++i) {
const item = settings.path ? Hoek.reach(value[i], settings.path) : value[i];
Expand Down Expand Up @@ -543,7 +550,7 @@ internals.Array = class extends Any {
records.set(item, i);
}
else {
if (records[item] !== undefined) {
if ((!ignoreUndefined || item !== undefined) && records[item] !== undefined) {
const localState = {
key: state.key,
path: state.path.concat(i),
Expand Down
56 changes: 56 additions & 0 deletions test/types/array.js
Expand Up @@ -1479,6 +1479,62 @@ describe('array', () => {
]);
});

it('ignores undefined value when ignoreUndefined is true', () => {

const schema = Joi.array().unique('a', { ignoreUndefined: true });

Helper.validate(schema, [
[[{ a: 'b' }, { a: 'c' }], true],
[[{ c: 'd' }, { c: 'd' }], true],
[[{ a: 'b', c: 'd' }, { a: 'b', c: 'd' }], false, null, {
message: '"value" position 1 contains a duplicate value',
details: [{
message: '"value" position 1 contains a duplicate value',
path: [1],
type: 'array.unique',
context: {
pos: 1,
value: { a: 'b', c: 'd' },
dupePos: 0,
dupeValue: { a: 'b', c: 'd' },
label: 'value',
key: 1,
path: 'a'
}
}]
}],
[[{ a: 'b', c: 'c' }, { a: 'b', c: 'd' }], false, null, {
message: '"value" position 1 contains a duplicate value',
details: [{
message: '"value" position 1 contains a duplicate value',
path: [1],
type: 'array.unique',
context: {
pos: 1,
value: { a: 'b', c: 'd' },
dupePos: 0,
dupeValue: { a: 'b', c: 'c' },
label: 'value',
key: 1,
path: 'a'
}
}]
}]
]);
});

it('fails with invalid configs', () => {

expect(() => {

Joi.array().unique('id', 'invalid configs');
}).to.throw(Error, 'configs must be an object');
expect(() => {

Joi.array().unique('id', {});
}).to.not.throw();
});

it('fails with invalid comparator', () => {

expect(() => {
Expand Down

0 comments on commit 83eb8eb

Please sign in to comment.