Skip to content

Commit

Permalink
Merge pull request #1499 from rgoble4/dynamic-keys
Browse files Browse the repository at this point in the history
Allow object keys to be verified by schema
  • Loading branch information
Marsup authored Jun 6, 2018
2 parents 8a1eb96 + eaefa17 commit d97ca0d
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 10 deletions.
1 change: 1 addition & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1603,6 +1603,7 @@ const schema = Joi.object().length(5);

Specify validation rules for unknown keys matching a pattern where:
- `regex` - a regular expression tested against the unknown key names.
- `regex` - may also be a schema object validated against the unknown key names.
- `schema` - the schema object matching keys must validate against.

```js
Expand Down
35 changes: 27 additions & 8 deletions lib/types/object/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,14 +249,24 @@ internals.Object = class extends Any {
for (let i = 0; i < this._inner.patterns.length; ++i) {
const pattern = this._inner.patterns[i];

if (pattern.regex.test(key)) {
let shouldProcess = false;

if (pattern.regex && pattern.patternRule.test(key)) {
shouldProcess = true;
}
else if (!pattern.regex) {
const keyResult = pattern.patternRule.validate(key);
shouldProcess = !keyResult.error;
}

if (shouldProcess) {
unprocessed.delete(key);

const result = pattern.rule._validate(item, localState, options);
const result = pattern.valueRule._validate(item, localState, options);
if (result.errors) {
errors.push(this.createError('object.child', {
key,
child: pattern.rule._getLabel(key),
child: pattern.valueRule._getLabel(key),
reason: result.errors
}, localState, options));

Expand Down Expand Up @@ -438,10 +448,15 @@ internals.Object = class extends Any {

pattern(pattern, schema) {

Hoek.assert(pattern instanceof RegExp, 'Invalid regular expression');
Hoek.assert(pattern instanceof RegExp || pattern instanceof Any, 'pattern must be a regex or schema');
Hoek.assert(schema !== undefined, 'Invalid rule');

pattern = new RegExp(pattern.source, pattern.ignoreCase ? 'i' : undefined); // Future version should break this and forbid unsupported regex flags
let regex = false;

if (pattern instanceof RegExp) {
pattern = new RegExp(pattern.source, pattern.ignoreCase ? 'i' : undefined); // Future version should break this and forbid unsupported regex flags
regex = true;
}

try {
schema = Cast.schema(this._currentJoi, schema);
Expand All @@ -454,9 +469,8 @@ internals.Object = class extends Any {
throw castErr;
}


const obj = this.clone();
obj._inner.patterns.push({ regex: pattern, rule: schema });
obj._inner.patterns.push({ patternRule: pattern, valueRule: schema, regex });
return obj;
}

Expand Down Expand Up @@ -642,7 +656,12 @@ internals.Object = class extends Any {

for (let i = 0; i < this._inner.patterns.length; ++i) {
const pattern = this._inner.patterns[i];
description.patterns.push({ regex: pattern.regex.toString(), rule: pattern.rule.describe() });
if (pattern.regex) {
description.patterns.push({ regex: pattern.patternRule.toString(), rule: pattern.valueRule.describe() });
}
else {
description.patterns.push({ regex: pattern.patternRule.describe(), rule: pattern.valueRule.describe() });
}
}
}

Expand Down
152 changes: 150 additions & 2 deletions test/types/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -1548,6 +1548,43 @@ describe('object', () => {
]
});
});

it('describes patterns with schema', () => {

const schema = Joi.object({
a: Joi.string()
}).pattern(Joi.string().uuid('uuidv4'), Joi.boolean());

expect(schema.describe()).to.equal({
type: 'object',
children: {
a: {
type: 'string',
invalids: ['']
}
},
patterns: [
{
regex: {
invalids: [''],
rules: [{
arg: 'uuidv4',
name: 'guid'
}],
type: 'string'
},
rule: {
type: 'boolean',
truthy: [true],
falsy: [false],
flags: {
insensitive: true
}
}
}
]
});
});
});

describe('length()', () => {
Expand Down Expand Up @@ -1609,7 +1646,7 @@ describe('object', () => {

});

it('validates unknown keys using a pattern', async () => {
it('validates unknown keys using a regex pattern', async () => {

const schema = Joi.object({
a: Joi.number()
Expand Down Expand Up @@ -1667,6 +1704,65 @@ describe('object', () => {
]);
});

it('validates unknown keys using a schema pattern', async () => {

const schema = Joi.object({
a: Joi.number()
}).pattern(Joi.number().positive(), Joi.boolean())
.pattern(Joi.string().length(2), 'x');

const err = await expect(Joi.validate({ bb: 'y', 5: 'x' }, schema, { abortEarly: false })).to.reject();
expect(err).to.be.an.error('child "5" fails because ["5" must be a boolean]. child "bb" fails because ["bb" must be one of [x]]');
expect(err.details).to.equal([
{
message: '"5" must be a boolean',
path: ['5'],
type: 'boolean.base',
context: { label: '5', key: '5' }
},
{
message: '"bb" must be one of [x]',
path: ['bb'],
type: 'any.allowOnly',
context: { value: 'y', valids: ['x'], label: 'bb', key: 'bb' }
}
]);

Helper.validate(schema, [
[{ a: 5 }, true],
[{ a: 'x' }, false, null, {
message: 'child "a" fails because ["a" must be a number]',
details: [{
message: '"a" must be a number',
path: ['a'],
type: 'number.base',
context: { label: 'a', key: 'a' }
}]
}],
[{ b: 'x' }, false, null, {
message: '"b" is not allowed',
details: [{
message: '"b" is not allowed',
path: ['b'],
type: 'object.allowUnknown',
context: { child: 'b', label: 'b', key: 'b' }
}]
}],
[{ bb: 'x' }, true],
[{ 5: 'x' }, false, null, {
message: 'child "5" fails because ["5" must be a boolean]',
details: [{
message: '"5" must be a boolean',
path: ['5'],
type: 'boolean.base',
context: { label: '5', key: '5' }
}]
}],
[{ 5: false }, true],
[{ 5: undefined }, true]
]);
});

it('validates unknown keys using a pattern (nested)', async () => {

const schema = {
Expand All @@ -1693,7 +1789,33 @@ describe('object', () => {
]);
});

it('errors when using a pattern on empty schema with unknown(false) and pattern mismatch', async () => {
it('validates unknown keys using a pattern (nested)', async () => {

const schema = {
x: Joi.object({
a: Joi.number()
}).pattern(Joi.number().positive(), Joi.boolean()).pattern(Joi.string().length(2), 'x')
};

const err = await expect(Joi.validate({ x: { bb: 'y', 5: 'x' } }, schema, { abortEarly: false })).to.reject();
expect(err).to.be.an.error('child "x" fails because [child "5" fails because ["5" must be a boolean], child "bb" fails because ["bb" must be one of [x]]]');
expect(err.details).to.equal([
{
message: '"5" must be a boolean',
path: ['x', '5'],
type: 'boolean.base',
context: { label: '5', key: '5' }
},
{
message: '"bb" must be one of [x]',
path: ['x', 'bb'],
type: 'any.allowOnly',
context: { value: 'y', valids: ['x'], label: 'bb', key: 'bb' }
}
]);
});

it('errors when using a pattern on empty schema with unknown(false) and regex pattern mismatch', async () => {

const schema = Joi.object().pattern(/\d/, Joi.number()).unknown(false);

Expand All @@ -1706,6 +1828,19 @@ describe('object', () => {
}]);
});

it('errors when using a pattern on empty schema with unknown(false) and schema pattern mismatch', async () => {

const schema = Joi.object().pattern(Joi.number().positive(), Joi.number()).unknown(false);

const err = await expect(Joi.validate({ a: 5 }, schema, { abortEarly: false })).to.reject('"a" is not allowed');
expect(err.details).to.equal([{
message: '"a" is not allowed',
path: ['a'],
type: 'object.allowUnknown',
context: { child: 'a', label: 'a', key: 'a' }
}]);
});

it('removes global flag from patterns', async () => {

const schema = Joi.object().pattern(/a/g, Joi.number());
Expand All @@ -1719,6 +1854,19 @@ describe('object', () => {
const value = await Joi.validate({ a1: undefined, a2: null, a3: 'test' }, schema);
expect(value).to.equal({ a1: undefined, a2: undefined, a3: 'test' });
});

it('should throw an error if pattern is not regex or instance of Any', () => {

let error;
try {
Joi.object().pattern(17, Joi.boolean());
error = false;
}
catch (e) {
error = true;
}
expect(error).to.equal(true);
});
});

describe('with()', () => {
Expand Down

0 comments on commit d97ca0d

Please sign in to comment.