From 8d7cd50306326e2919c6711c242478238242785c Mon Sep 17 00:00:00 2001 From: Doug McCall Date: Sun, 5 Aug 2018 09:43:00 -0400 Subject: [PATCH 1/4] Support labels for nested objects --- lib/types/object/index.js | 14 +++- test/types/object.js | 157 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 1 deletion(-) diff --git a/lib/types/object/index.js b/lib/types/object/index.js index fa72a91b0..32c0de80b 100644 --- a/lib/types/object/index.js +++ b/lib/types/object/index.js @@ -765,14 +765,26 @@ internals.keysToLabels = function (schema, keys) { return keys; } + const findNestedLabel = function (key) { + + const [parentKey, ...parts] = key.split('.'); + const childKeys = parts.join('.'); + const childSchema = children.find((child) => child.key === parentKey); + const label = internals.keysToLabels(childSchema.schema, childKeys); + return key.endsWith(label) ? key : label; + }; + const findLabel = function (key) { + if (key.includes('.')) { + return findNestedLabel(key); + } const matchingChild = children.find((child) => child.key === key); return matchingChild ? matchingChild.schema._getLabel(key) : key; }; if (Array.isArray(keys)) { - return keys.map(findLabel); + return [].concat(keys.map(findLabel)); } return findLabel(keys); diff --git a/test/types/object.js b/test/types/object.js index 9484c1390..37d38d7c1 100644 --- a/test/types/object.js +++ b/test/types/object.js @@ -1967,6 +1967,29 @@ describe('object', () => { }); }); + it('should apply labels with nested objects', () => { + + const schema = Joi.object({ + a: Joi.number().label('first'), + b: Joi.object({ c: Joi.string().label('second'), d: Joi.number() }) + }).with('a', ['b.c']); + const error = schema.validate({ a: 1 , b: { d: 2 } }).error; + expect(error).to.be.an.error('"first" missing required peer "second"'); + expect(error.details).to.equal([{ + message: '"first" missing required peer "second"', + path: ['a'], + type: 'object.with', + context: { + main: 'a', + mainWithLabel: 'first', + peer: 'b.c', + peerWithLabel: 'second', + label: 'a', + key: 'a' + } + }]); + }); + describe('without()', () => { it('should throw an error when a parameter is not a string', () => { @@ -2076,6 +2099,29 @@ describe('object', () => { } }]); }); + + it('should apply labels with nested objects', () => { + + const schema = Joi.object({ + a: Joi.number().label('first'), + b: Joi.object({ c: Joi.string().label('second'), d: Joi.number() }) + }).without('a', ['b.c']); + const error = schema.validate({ a: 1, b: { c: 'c' } }).error; + expect(error).to.be.an.error('"first" conflict with forbidden peer "second"'); + expect(error.details).to.equal([{ + message: '"first" conflict with forbidden peer "second"', + path: ['a'], + type: 'object.without', + context: { + main: 'a', + mainWithLabel: 'first', + peer: 'b.c', + peerWithLabel: 'second', + label: 'a', + key: 'a' + } + }]); + }); }); describe('xor()', () => { @@ -2172,6 +2218,48 @@ describe('object', () => { } }]); }); + + it('should apply labels without any peer', () => { + + const schema = Joi.object({ + a: Joi.number().label('first'), + b: Joi.object({ c: Joi.string().label('second'), d: Joi.number() }) + }).xor('a', 'b.c'); + const error = schema.validate({}).error; + expect(error).to.be.an.error('"value" must contain at least one of [first, second]'); + expect(error.details).to.equal([{ + message: '"value" must contain at least one of [first, second]', + path: [], + type: 'object.missing', + context: { + peers: ['a', 'b.c'], + peersWithLabels: ['first', 'second'], + label: 'value', + key: undefined + } + }] ); + }); + + it('should apply labels with too many peers', () => { + + const schema = Joi.object({ + a: Joi.number().label('first'), + b: Joi.object({ c: Joi.string().label('second'), d: Joi.number() }) + }).xor('a', 'b.c'); + const error = schema.validate({ a: 1, b: { c: 'c' } }).error; + expect(error).to.be.an.error('"value" contains a conflict between exclusive peers [first, second]'); + expect(error.details).to.equal([{ + message: '"value" contains a conflict between exclusive peers [first, second]', + path: [], + type: 'object.xor', + context: { + peers: ['a', 'b.c'], + peersWithLabels: ['first', 'second'], + label: 'value', + key: undefined + } + }]); + }); }); describe('or()', () => { @@ -2270,6 +2358,28 @@ describe('object', () => { } }]); }); + + it('should apply labels with nested objects', () => { + + const schema = Joi.object({ + a: Joi.number().label('first'), + b: Joi.object({ c: Joi.string().label('second'), d: Joi.number() }) + }).or('a', 'b.c'); + const error = schema.validate({}).error; + expect(error).to.be.an.error('"value" must contain at least one of [first, second]'); + expect(error.details).to.equal([{ + message: '"value" must contain at least one of [first, second]', + path: [], + type: 'object.missing', + context: + { + peers: ['a', 'b.c'], + peersWithLabels: ['first', 'second'], + label: 'value', + key: undefined + } + }]); + }); }); describe('and()', () => { @@ -2328,6 +2438,30 @@ describe('object', () => { } }]); }); + + it('should apply labels with nested objects', () => { + + const schema = Joi.object({ + a: Joi.number().label('first'), + b: Joi.object({ c: Joi.string().label('second'), d: Joi.number() }) + }).and('a', 'b.c'); + const error = schema.validate({ a: 1 }).error; + expect(error).to.be.an.error('"value" contains [first] without its required peers [second]'); + expect(error.details).to.equal([{ + message: '"value" contains [first] without its required peers [second]', + path: [], + type: 'object.and', + context: + { + present: ['a'], + presentWithLabels: ['first'], + missing: ['b.c'], + missingWithLabels: ['second'], + label: 'value', + key: undefined + } + }]); + }); }); describe('nand()', () => { @@ -2385,6 +2519,29 @@ describe('object', () => { } }]); }); + + it('should apply labels with nested objects', () => { + + const schema = Joi.object({ + a: Joi.number().label('first'), + b: Joi.object({ c: Joi.string().label('second'), d: Joi.number() }) + }).nand('a', 'b.c'); + const error = schema.validate({ a: 1, b: { c: 'c' } }).error; + expect(error).to.be.an.error('"first" must not exist simultaneously with [second]'); + expect(error.details).to.equal([{ + message: '"first" must not exist simultaneously with [second]', + path: [], + type: 'object.nand', + context: { + main: 'a', + mainWithLabel: 'first', + peers: ['b.c'], + peersWithLabels: ['second'], + label: 'value', + key: undefined + } + }]); + }); }); describe('assert()', () => { From 23ecd9022ca1c09c682039b840ca1f956733af1b Mon Sep 17 00:00:00 2001 From: Doug McCall Date: Sun, 5 Aug 2018 09:47:53 -0400 Subject: [PATCH 2/4] Clean up nested label logic comprehension --- lib/types/object/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/types/object/index.js b/lib/types/object/index.js index 32c0de80b..4130c555c 100644 --- a/lib/types/object/index.js +++ b/lib/types/object/index.js @@ -769,9 +769,10 @@ internals.keysToLabels = function (schema, keys) { const [parentKey, ...parts] = key.split('.'); const childKeys = parts.join('.'); - const childSchema = children.find((child) => child.key === parentKey); - const label = internals.keysToLabels(childSchema.schema, childKeys); - return key.endsWith(label) ? key : label; + const matchingChild = children.find((child) => child.key === parentKey); + const label = matchingChild ? internals.keysToLabels(matchingChild.schema, childKeys) : key; + + return !key.endsWith(label) ? label : key; }; const findLabel = function (key) { From d804d253a00e70eb89c2720f8f8d97dd2a825ece Mon Sep 17 00:00:00 2001 From: Doug McCall Date: Sun, 5 Aug 2018 09:57:52 -0400 Subject: [PATCH 3/4] Add nested peer test for better coverage --- test/types/object.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/test/types/object.js b/test/types/object.js index 37d38d7c1..b10fa0b84 100644 --- a/test/types/object.js +++ b/test/types/object.js @@ -2219,7 +2219,7 @@ describe('object', () => { }]); }); - it('should apply labels without any peer', () => { + it('should apply labels without any nested peers', () => { const schema = Joi.object({ a: Joi.number().label('first'), @@ -2240,7 +2240,7 @@ describe('object', () => { }] ); }); - it('should apply labels with too many peers', () => { + it('should apply labels with too many nested peers', () => { const schema = Joi.object({ a: Joi.number().label('first'), @@ -2462,6 +2462,30 @@ describe('object', () => { } }]); }); + + it('should apply labels with invalid nested peers', () => { + + const schema = Joi.object({ + a: Joi.number().label('first'), + b: Joi.object({ c: Joi.string().label('second'), d: Joi.number() }) + }).and('a', 'c.d'); + const error = schema.validate({ a: 1, b: { d: 1 } }).error; + expect(error).to.be.an.error('"value" contains [first] without its required peers [c.d]'); + expect(error.details).to.equal([{ + message: '"value" contains [first] without its required peers [c.d]', + path: [], + type: 'object.and', + context: + { + present: ['a'], + presentWithLabels: ['first'], + missing: ['c.d'], + missingWithLabels: ['c.d'], + label: 'value', + key: undefined + } + }]); + }); }); describe('nand()', () => { From d6c0aa9afcb408f47d4b4a1f848f41825312183b Mon Sep 17 00:00:00 2001 From: Doug McCall Date: Sun, 5 Aug 2018 16:12:16 -0400 Subject: [PATCH 4/4] Simplify nested findLabel logic --- lib/types/object/index.js | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/lib/types/object/index.js b/lib/types/object/index.js index 4130c555c..d37c9bd2d 100644 --- a/lib/types/object/index.js +++ b/lib/types/object/index.js @@ -765,27 +765,14 @@ internals.keysToLabels = function (schema, keys) { return keys; } - const findNestedLabel = function (key) { - - const [parentKey, ...parts] = key.split('.'); - const childKeys = parts.join('.'); - const matchingChild = children.find((child) => child.key === parentKey); - const label = matchingChild ? internals.keysToLabels(matchingChild.schema, childKeys) : key; - - return !key.endsWith(label) ? label : key; - }; - const findLabel = function (key) { - if (key.includes('.')) { - return findNestedLabel(key); - } - const matchingChild = children.find((child) => child.key === key); - return matchingChild ? matchingChild.schema._getLabel(key) : key; + const matchingChild = schema._currentJoi.reach(schema, key); + return matchingChild ? matchingChild._getLabel(key) : key; }; if (Array.isArray(keys)) { - return [].concat(keys.map(findLabel)); + return keys.map(findLabel); } return findLabel(keys);