Skip to content

Commit

Permalink
Add object().oxor(). Closes #1628
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse authored and Marsup committed Nov 19, 2018
1 parent a9aa3e7 commit 76dcb36
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 37 deletions.
14 changes: 14 additions & 0 deletions API.md
Expand Up @@ -1961,6 +1961,20 @@ const schema = Joi.object().keys({

💥 Possible validation errors:[`object.xor`](#objectxor), [`object.missing`](#objectmissing)

#### `object.oxor(...peers)`

Defines an exclusive relationship between a set of keys where only one is allowed but non are required where:
- `peers` - the exclusive key names that must not appear together but where none are required.

```js
const schema = Joi.object().keys({
a: Joi.any(),
b: Joi.any()
}).oxor('a', 'b');
```

💥 Possible validation errors:[`object.xor`](#objectxor), [`object.missing`](#objectmissing)

#### `object.with(key, peers)`

Requires the presence of other keys whenever the specified key is present where:
Expand Down
1 change: 1 addition & 0 deletions lib/language.js 100644 → 100755
Expand Up @@ -92,6 +92,7 @@ exports.errors = {
without: '!!"{{mainWithLabel}}" conflict with forbidden peer "{{peerWithLabel}}"',
missing: 'must contain at least one of {{peersWithLabels}}',
xor: 'contains a conflict between exclusive peers {{peersWithLabels}}',
oxor: 'contains a conflict between optional exclusive peers {{peersWithLabels}}',
and: 'contains {{presentWithLabels}} without its required peers {{missingWithLabels}}',
nand: '!!"{{mainWithLabel}}" must not exist simultaneously with {{peersWithLabels}}',
assert: '!!"{{ref}}" validation failed because "{{ref}}" failed to {{message}}',
Expand Down
31 changes: 30 additions & 1 deletion lib/types/object/index.js 100644 → 100755
Expand Up @@ -503,6 +503,11 @@ internals.Object = class extends Any {
return this._dependency('xor', null, peers);
}

oxor(...peers) {

return this._dependency('oxor', null, peers);
}

or(...peers) {

peers = Hoek.flatten(peers);
Expand Down Expand Up @@ -831,7 +836,6 @@ internals.xor = function (key, value, peers, parent, state, options) {
const peer = peers[i];
const keysExist = Hoek.reach(parent, peer);
if (keysExist !== undefined) {

present.push(peer);
}
}
Expand All @@ -853,6 +857,31 @@ internals.xor = function (key, value, peers, parent, state, options) {
};


internals.oxor = function (key, value, peers, parent, state, options) {

const present = [];
for (let i = 0; i < peers.length; ++i) {
const peer = peers[i];
const keysExist = Hoek.reach(parent, peer);
if (keysExist !== undefined) {
present.push(peer);
}
}

if (!present.length ||
present.length === 1) {

return;
}

const context = { peers, peersWithLabels: internals.keysToLabels(this, peers) };
context.present = present;
context.presentWithLabels = internals.keysToLabels(this, present);

return this.createError('object.oxor', context, state, options);
};


internals.or = function (key, value, peers, parent, state, options) {

for (let i = 0; i < peers.length; ++i) {
Expand Down
164 changes: 128 additions & 36 deletions test/types/object.js 100644 → 100755
Expand Up @@ -2406,6 +2406,98 @@ describe('object', () => {
});
});

describe('oxor()', () => {

it('should throw an error when a parameter is not a string', () => {

let error;
try {
Joi.object().oxor({});
error = false;
}
catch (e) {
error = true;
}

expect(error).to.equal(true);

try {
Joi.object().oxor(123);
error = false;
}
catch (e) {
error = true;
}

expect(error).to.equal(true);
});

it('allows none of optional peers', () => {

const schema = Joi.object({
a: Joi.number(),
b: Joi.string()
}).oxor('a', 'b');

const error = schema.validate({}).error;
expect(error).to.not.exist();
});

it('should apply labels with too many peers', () => {

const schema = Joi.object({
a: Joi.number().label('first'),
b: Joi.string().label('second')
}).oxor('a', 'b');
const error = schema.validate({ a: 1, b: 'b' }).error;
expect(error).to.be.an.error('"value" contains a conflict between optional exclusive peers [first, second]');
expect(error.details).to.equal([{
message: '"value" contains a conflict between optional exclusive peers [first, second]',
path: [],
type: 'object.oxor',
context: {
peers: ['a', 'b'],
peersWithLabels: ['first', 'second'],
present: ['a', 'b'],
presentWithLabels: ['first', 'second'],
label: 'value',
key: undefined
}
}]);
});

it('should support nested objects', () => {

const schema = Joi.object({
a: Joi.string(),
b: Joi.object({ c: Joi.string(), d: Joi.number() }),
d: Joi.number()
}).oxor('a', 'b.c');

const sampleObject = { a: 'test', b: { d: 80 } };
const sampleObject2 = { a: 'test', b: { c: 'test2' } };

const error = schema.validate(sampleObject).error;
expect(error).to.equal(null);

const error2 = schema.validate(sampleObject2).error;
expect(error2).to.be.an.error('"value" contains a conflict between optional exclusive peers [a, b.c]');
expect(error2.details).to.equal([{
message: '"value" contains a conflict between optional exclusive peers [a, b.c]',
path: [],
type: 'object.oxor',
context: {
peers: ['a', 'b.c'],
peersWithLabels: ['a', 'b.c'],
present: ['a', 'b.c'],
presentWithLabels: ['a', 'b.c'],
key: undefined,
label: 'value'
}
}]);
});
});

describe('or()', () => {

it('should throw an error when a parameter is not a string', () => {
Expand Down Expand Up @@ -2467,12 +2559,12 @@ describe('object', () => {
path: [],
type: 'object.missing',
context:
{
peers: ['a', 'b'],
peersWithLabels: ['first', 'second'],
label: 'value',
key: undefined
}
{
peers: ['a', 'b'],
peersWithLabels: ['first', 'second'],
label: 'value',
key: undefined
}
}]);
});

Expand Down Expand Up @@ -2518,12 +2610,12 @@ describe('object', () => {
path: [],
type: 'object.missing',
context:
{
peers: ['a', 'b.c'],
peersWithLabels: ['first', 'second'],
label: 'value',
key: undefined
}
{
peers: ['a', 'b.c'],
peersWithLabels: ['first', 'second'],
label: 'value',
key: undefined
}
}]);
});
});
Expand All @@ -2543,14 +2635,14 @@ describe('object', () => {
path: [],
type: 'object.and',
context:
{
present: ['a'],
presentWithLabels: ['first'],
missing: ['b'],
missingWithLabels: ['second'],
label: 'value',
key: undefined
}
{
present: ['a'],
presentWithLabels: ['first'],
missing: ['b'],
missingWithLabels: ['second'],
label: 'value',
key: undefined
}
}]);
});

Expand Down Expand Up @@ -2598,14 +2690,14 @@ describe('object', () => {
path: [],
type: 'object.and',
context:
{
present: ['a'],
presentWithLabels: ['first'],
missing: ['b.c'],
missingWithLabels: ['second'],
label: 'value',
key: undefined
}
{
present: ['a'],
presentWithLabels: ['first'],
missing: ['b.c'],
missingWithLabels: ['second'],
label: 'value',
key: undefined
}
}]);
});

Expand All @@ -2622,14 +2714,14 @@ describe('object', () => {
path: [],
type: 'object.and',
context:
{
present: ['a'],
presentWithLabels: ['first'],
missing: ['c.d'],
missingWithLabels: ['c.d'],
label: 'value',
key: undefined
}
{
present: ['a'],
presentWithLabels: ['first'],
missing: ['c.d'],
missingWithLabels: ['c.d'],
label: 'value',
key: undefined
}
}]);
});
});
Expand Down

0 comments on commit 76dcb36

Please sign in to comment.