Skip to content

Commit

Permalink
Add ordered API method for array to match items against input in order.
Browse files Browse the repository at this point in the history
closes hapijs#659
  • Loading branch information
ck-lee committed Oct 5, 2015
1 parent 6bb5c72 commit 765a24b
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 4 deletions.
15 changes: 15 additions & 0 deletions API.md
Expand Up @@ -38,6 +38,7 @@
- [`array.sparse(enabled)`](#arraysparseenabled)
- [`array.single(enabled)`](#arraysingleenabled)
- [`array.items(type)`](#arrayitemstype)
- [`array.ordered(type)`](#arrayorderedtype)
- [`array.min(limit)`](#arrayminlimit)
- [`array.max(limit)`](#arraymaxlimit)
- [`array.length(limit)`](#arraylengthlimit)
Expand Down Expand Up @@ -547,6 +548,20 @@ var schema = Joi.array().items(Joi.string().valid('not allowed').forbidden(), Jo
var schema = Joi.array().items(Joi.string().label('My string').required(), Joi.number().required()); // If this fails it can result in `[ValidationError: "value" does not contain [My string] and 1 other required value(s)]`
```

#### `array.ordered(type)`

List the types in sequence order for the array values where:
- `type` - a **joi** schema object to validate against each array item in sequence order. `type` can be an array of values, or multiple values can be passed as individual arguments.

If a given type is `.required()` then there must be a matching item with the same index position in the array.
Errors will contain the number of items that didn't match. Any unmatched item having a [label](#anylabelname) will be mentioned explicitly.

```javascript
var schema = Joi.array().ordered(Joi.string().required(), Joi.number().required()); // array must have first item as string and second item as number
var schema = Joi.array().ordered(Joi.string().required()).items(Joi.number().required()); // array must have first item as string and 1 or more subsequent items as number
var schema = Joi.array().ordered(Joi.string().required(), Joi.number()); // array must have first item as string and optionally second item as number
```

#### `array.min(limit)`

Specifies the minimum number of items in the array where:
Expand Down
69 changes: 68 additions & 1 deletion lib/array.js
Expand Up @@ -29,6 +29,7 @@ internals.Array = function () {
Any.call(this);
this._type = 'array';
this._inner.items = [];
this._inner.ordereds = [];
this._inner.inclusions = [];
this._inner.exclusions = [];
this._inner.requireds = [];
Expand Down Expand Up @@ -106,6 +107,7 @@ internals.checkItems = function (items, wasArray, state, options) {
var errored;

var requireds = this._inner.requireds.slice();
var ordereds = this._inner.ordereds.slice();
var inclusions = this._inner.inclusions.concat(requireds);

for (var v = 0, vl = items.length; v < vl; ++v) {
Expand All @@ -131,6 +133,7 @@ internals.checkItems = function (items, wasArray, state, options) {

for (var i = 0, il = this._inner.exclusions.length; i < il; ++i) {
res = this._inner.exclusions[i]._validate(item, localState, {}); // Not passing options to use defaults

if (!res.errors) {
errors.push(Errors.create(wasArray ? 'array.excludes' : 'array.excludesSingle', { pos: v, value: item }, { key: state.key, path: localState.path }, options));
errored = true;
Expand All @@ -147,6 +150,38 @@ internals.checkItems = function (items, wasArray, state, options) {
continue;
}

// Ordered
if (this._inner.ordereds.length) {
if (ordereds.length > 0) {
var ordered = ordereds.shift();
res = ordered._validate(item, localState, options);
if (!res.errors) {
if (ordered._flags.strip) {
internals.fastSplice(items, v);
--v;
--vl;
}
else {
items[v] = res.value;
}
}
else {
errors.push(Errors.create('array.ordered', { pos: v, reason: res.errors, value: item }, { key: state.key, path: localState.path }, options));
if (options.abortEarly) {
return errors;
}
}
continue;
}
else if (!this._inner.items.length) {
errors.push(Errors.create('array.orderedLength', { pos: v, limit: this._inner.ordereds.length }, { key: state.key, path: localState.path }, options));
if (options.abortEarly) {
return errors;
}
continue;
}
}

// Requireds

var requiredChecks = [];
Expand Down Expand Up @@ -238,10 +273,13 @@ internals.checkItems = function (items, wasArray, state, options) {
internals.fillMissedErrors(errors, requireds, state, options);
}

if (ordereds.length) {
internals.fillOrderedErrors(errors, ordereds, state, options);
}

return errors.length ? errors : null;
};


internals.fillMissedErrors = function (errors, requireds, state, options) {

var knownMisses = [];
Expand Down Expand Up @@ -269,6 +307,21 @@ internals.fillMissedErrors = function (errors, requireds, state, options) {
}
};

internals.fillOrderedErrors = function (errors, ordereds, state, options) {

var requiredOrdereds = [];

for (var i = 0, il = ordereds.length; i < il; ++i) {
var presence = Hoek.reach(ordereds[i], '_flags.presence');
if (presence === 'required') {
requiredOrdereds.push(ordereds[i]);
}
}

if (requiredOrdereds.length) {
internals.fillMissedErrors(errors, requiredOrdereds, state, options);
}
};

internals.Array.prototype.describe = function () {

Expand Down Expand Up @@ -310,6 +363,20 @@ internals.Array.prototype.items = function () {
};


internals.Array.prototype.ordered = function () {

var obj = this.clone();

Hoek.flatten(Array.prototype.slice.call(arguments)).forEach(function (type) {

type = Cast.schema(type);
obj._inner.ordereds.push(type);
});

return obj;
};


internals.Array.prototype.min = function (limit) {

Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer');
Expand Down
2 changes: 2 additions & 0 deletions lib/language.js
Expand Up @@ -37,6 +37,8 @@ exports.errors = {
min: 'must contain at least {{limit}} items',
max: 'must contain less than or equal to {{limit}} items',
length: 'must contain {{limit}} items',
ordered: 'at position {{pos}} fails because {{reason}}',
orderedLength: 'at position {{pos}} fails because array must contain at most {{limit}} items',
sparse: 'must not be a sparse array',
unique: 'position {{pos}} contains a duplicate value'
},
Expand Down
202 changes: 202 additions & 0 deletions test/array.js
Expand Up @@ -793,4 +793,206 @@ describe('array', function () {
});
});
});

describe('#ordered', function () {

it('validates input against items in order', function (done) {

var schema = Joi.array().ordered([Joi.string().required(), Joi.number().required()]);
var input = ['s1', 2];
schema.validate(input, function (err, value) {

expect(err).to.not.exist();
expect(value).to.deep.equal(['s1', 2]);
done();
});
});

it('validates input with optional item', function (done) {

var schema = Joi.array().ordered([Joi.string().required(), Joi.number().required(), Joi.number()]);
var input = ['s1', 2, 3];

schema.validate(input, function (err, value) {

expect(err).to.not.exist();
expect(value).to.deep.equal(['s1', 2, 3]);
done();
});
});

it('validates input without optional item', function (done) {

var schema = Joi.array().ordered([Joi.string().required(), Joi.number().required(), Joi.number()]);
var input = ['s1', 2];

schema.validate(input, function (err, value) {

expect(err).to.not.exist();
expect(value).to.deep.equal(['s1', 2]);
done();
});
});

it('validates input without optional item', function (done) {

var schema = Joi.array().ordered([Joi.string().required(), Joi.number().required(), Joi.number()]).sparse(true);
var input = ['s1', 2, undefined];

schema.validate(input, function (err, value) {

expect(err).to.not.exist();
expect(value).to.deep.equal(['s1', 2, undefined]);
done();
});
});

it('validates input without optional item in a sparse array', function (done) {

var schema = Joi.array().ordered([Joi.string().required(), Joi.number(), Joi.number().required()]).sparse(true);
var input = ['s1', undefined, 3];

schema.validate(input, function (err, value) {

expect(err).to.not.exist();
expect(value).to.deep.equal(['s1', undefined, 3]);
done();
});
});

it('validates when input matches ordered items and matches regular items', function (done) {

var schema = Joi.array().ordered([Joi.string().required(), Joi.number().required()]).items(Joi.number());
var input = ['s1', 2, 3, 4, 5];
schema.validate(input, function (err, value) {

expect(err).to.not.exist();
expect(value).to.deep.equal(['s1', 2, 3, 4, 5]);
done();
});
});

it('errors when input does not match ordered items', function (done) {

var schema = Joi.array().ordered([Joi.number().required(), Joi.string().required()]);
var input = ['s1', 2];
schema.validate(input, function (err, value) {

expect(err).to.exist();
expect(err.message).to.equal('"value" at position 0 fails because ["0" must be a number]');
done();
});
});

it('errors when input has more items than ordered items', function (done) {

var schema = Joi.array().ordered([Joi.number().required(), Joi.string().required()]);
var input = [1, 's2', 3];
schema.validate(input, function (err, value) {

expect(err).to.exist();
expect(err.message).to.equal('"value" at position 2 fails because array must contain at most 2 items');
done();
});
});

it('errors when input has more items than ordered items with abortEarly = false', function (done) {

var schema = Joi.array().ordered([Joi.string(), Joi.number()]).options({ abortEarly: false });
var input = [1, 2, 3, 4, 5];
schema.validate(input, function (err, value) {

expect(err).to.exist();
expect(err.message).to.equal('"value" at position 0 fails because ["0" must be a string]. "value" at position 2 fails because array must contain at most 2 items. "value" at position 3 fails because array must contain at most 2 items. "value" at position 4 fails because array must contain at most 2 items');
expect(err.details).to.have.length(4);
done();
});
});

it('errors when input has less items than ordered items', function (done) {

var schema = Joi.array().ordered([Joi.number().required(), Joi.string().required()]);
var input = [1];
schema.validate(input, function (err, value) {

expect(err).to.exist();
expect(err.message).to.equal('"value" does not contain 1 required value(s)');
done();
});
});

it('errors when input matches ordered items but not matches regular items', function (done) {

var schema = Joi.array().ordered([Joi.string().required(), Joi.number().required()]).items(Joi.number()).options({ abortEarly: false });
var input = ['s1', 2, 3, 4, 's5'];
schema.validate(input, function (err, value) {

expect(err).to.exist();
expect(err.message).to.equal('"value" at position 4 fails because ["4" must be a number]');
done();
});
});

it('errors when input does not match ordered items but matches regular items', function (done) {

var schema = Joi.array().ordered([Joi.string(), Joi.number()]).items(Joi.number()).options({ abortEarly: false });
var input = [1, 2, 3, 4, 5];
schema.validate(input, function (err, value) {

expect(err).to.exist();
expect(err.message).to.equal('"value" at position 0 fails because ["0" must be a string]');
done();
});
});

it('errors when input does not match ordered items not matches regular items', function (done) {

var schema = Joi.array().ordered([Joi.string(), Joi.number()]).items(Joi.string()).options({ abortEarly: false });
var input = [1, 2, 3, 4, 5];
schema.validate(input, function (err, value) {

expect(err).to.exist();
expect(err.message).to.equal('"value" at position 0 fails because ["0" must be a string]. "value" at position 2 fails because ["2" must be a string]. "value" at position 3 fails because ["3" must be a string]. "value" at position 4 fails because ["4" must be a string]');
expect(err.details).to.have.length(4);
done();
});
});

it('errors but continues when abortEarly is set to false', function (done) {

var schema = Joi.array().ordered([Joi.number().required(), Joi.string().required()]).options({ abortEarly: false });
var input = ['s1', 2];
schema.validate(input, function (err, value) {

expect(err).to.exist();
expect(err.message).to.equal('"value" at position 0 fails because ["0" must be a number]. "value" at position 1 fails because ["1" must be a string]');
expect(err.details).to.have.length(2);
done();
});
});

it('strips item', function (done) {

var schema = Joi.array().ordered([Joi.string().required(), Joi.number().strip(), Joi.number().required()]);
var input = ['s1', 2, 3];
schema.validate(input, function (err, value) {

expect(err).to.not.exist();
expect(value).to.deep.equal(['s1', 3]);
done();
});
});

it('strips multiple items', function (done) {

var schema = Joi.array().ordered([Joi.string().strip(), Joi.number(), Joi.number().strip()]);
var input = ['s1', 2, 3];
schema.validate(input, function (err, value) {

expect(err).to.not.exist();
expect(value).to.deep.equal([2]);
done();
});
});
});
});
6 changes: 3 additions & 3 deletions test/errors.js
Expand Up @@ -287,7 +287,7 @@ describe('errors', function () {
it('annotates error within array multiple times on the same element', function (done) {

var object = {
a: [2, 3 , 4]
a: [2, 3, 4]
};

var schema = {
Expand Down Expand Up @@ -323,8 +323,8 @@ describe('errors', function () {
it('annotates error within multiple arrays and multiple times on the same element', function (done) {

var object = {
a: [2, 3 , 4],
b: [2, 3 , 4]
a: [2, 3, 4],
b: [2, 3, 4]
};

var schema = {
Expand Down

0 comments on commit 765a24b

Please sign in to comment.