From b74701655a2464f894c419b9836c9c8e4689f5df Mon Sep 17 00:00:00 2001 From: Rob Goble Date: Fri, 25 May 2018 18:02:35 -0500 Subject: [PATCH 1/3] Allow pattern to support schema objects --- lib/types/object/index.js | 35 +++++++-- test/types/object.js | 156 +++++++++++++++++++++++++++++++++++++- 2 files changed, 179 insertions(+), 12 deletions(-) diff --git a/lib/types/object/index.js b/lib/types/object/index.js index 18ce74b13..e5fe508af 100644 --- a/lib/types/object/index.js +++ b/lib/types/object/index.js @@ -244,14 +244,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)); @@ -433,10 +443,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); @@ -449,9 +464,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; } @@ -637,7 +651,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({ patternRule: pattern.patternRule.toString(), valueRule: pattern.valueRule.describe() }); + } + else { + description.patterns.push({ patternRule: pattern.patternRule.describe(), valueRule: pattern.valueRule.describe() }); + } } } diff --git a/test/types/object.js b/test/types/object.js index 01d5b3d98..5addb1e21 100644 --- a/test/types/object.js +++ b/test/types/object.js @@ -1535,8 +1535,45 @@ describe('object', () => { }, patterns: [ { - regex: '/\\w\\d/i', - rule: { + patternRule: '/\\w\\d/i', + valueRule: { + type: 'boolean', + truthy: [true], + falsy: [false], + flags: { + insensitive: true + } + } + } + ] + }); + }); + + 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: [ + { + patternRule: { + invalids: [''], + rules: [{ + arg: 'uuidv4', + name: 'guid' + }], + type: 'string' + }, + valueRule: { type: 'boolean', truthy: [true], falsy: [false], @@ -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() @@ -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 = { @@ -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); @@ -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()); @@ -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()', () => { From 3a84adc563655369b01e4864c40f14c199ce0eb6 Mon Sep 17 00:00:00 2001 From: Rob Goble Date: Fri, 25 May 2018 18:22:54 -0500 Subject: [PATCH 2/3] update docs --- API.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/API.md b/API.md index 1102760ae..5696a8e51 100644 --- a/API.md +++ b/API.md @@ -1611,6 +1611,18 @@ const schema = Joi.object({ }).pattern(/\w\d/, Joi.boolean()); ``` +#### `object.pattern(keySchema, valueSchema)` + +Specify validation rules for unknown keys matching a pattern where: +- `keySchema` - the schema object validated against the unknown key names. +- `valueSchema` - the schema object matching keys must validate against. + +```js +const schema = Joi.object({ + a: Joi.string() +}).pattern(Joi.string(), Joi.boolean()); +``` + #### `object.and(peers)` Defines an all-or-nothing relationship between keys where if one of the peers is present, all of them are required as From eaefa17b2e7abcd96692449bc7945d7997ccea55 Mon Sep 17 00:00:00 2001 From: Rob Goble Date: Sun, 27 May 2018 21:33:15 -0500 Subject: [PATCH 3/3] review changes --- API.md | 13 +------------ lib/types/object/index.js | 4 ++-- test/types/object.js | 8 ++++---- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/API.md b/API.md index 5696a8e51..76c1e9868 100644 --- a/API.md +++ b/API.md @@ -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 @@ -1611,18 +1612,6 @@ const schema = Joi.object({ }).pattern(/\w\d/, Joi.boolean()); ``` -#### `object.pattern(keySchema, valueSchema)` - -Specify validation rules for unknown keys matching a pattern where: -- `keySchema` - the schema object validated against the unknown key names. -- `valueSchema` - the schema object matching keys must validate against. - -```js -const schema = Joi.object({ - a: Joi.string() -}).pattern(Joi.string(), Joi.boolean()); -``` - #### `object.and(peers)` Defines an all-or-nothing relationship between keys where if one of the peers is present, all of them are required as diff --git a/lib/types/object/index.js b/lib/types/object/index.js index e5fe508af..dbd4f9ec5 100644 --- a/lib/types/object/index.js +++ b/lib/types/object/index.js @@ -652,10 +652,10 @@ internals.Object = class extends Any { for (let i = 0; i < this._inner.patterns.length; ++i) { const pattern = this._inner.patterns[i]; if (pattern.regex) { - description.patterns.push({ patternRule: pattern.patternRule.toString(), valueRule: pattern.valueRule.describe() }); + description.patterns.push({ regex: pattern.patternRule.toString(), rule: pattern.valueRule.describe() }); } else { - description.patterns.push({ patternRule: pattern.patternRule.describe(), valueRule: pattern.valueRule.describe() }); + description.patterns.push({ regex: pattern.patternRule.describe(), rule: pattern.valueRule.describe() }); } } } diff --git a/test/types/object.js b/test/types/object.js index 5addb1e21..261ac8c91 100644 --- a/test/types/object.js +++ b/test/types/object.js @@ -1535,8 +1535,8 @@ describe('object', () => { }, patterns: [ { - patternRule: '/\\w\\d/i', - valueRule: { + regex: '/\\w\\d/i', + rule: { type: 'boolean', truthy: [true], falsy: [false], @@ -1565,7 +1565,7 @@ describe('object', () => { }, patterns: [ { - patternRule: { + regex: { invalids: [''], rules: [{ arg: 'uuidv4', @@ -1573,7 +1573,7 @@ describe('object', () => { }], type: 'string' }, - valueRule: { + rule: { type: 'boolean', truthy: [true], falsy: [false],