Skip to content

Commit

Permalink
Merge pull request #1647 from hapijs/1628
Browse files Browse the repository at this point in the history
Add object().oxor(). Closes #1628
  • Loading branch information
Marsup committed Nov 19, 2018
2 parents a9aa3e7 + 3a38d9a commit 4653c3c
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 44 deletions.
36 changes: 34 additions & 2 deletions API.md
Expand Up @@ -105,6 +105,7 @@
- [`object.nand(peers)`](#objectnandpeers)
- [`object.or(peers)`](#objectorpeers)
- [`object.xor(peers)`](#objectxorpeers)
- [`object.oxor(...peers)`](#objectoxorpeers)
- [`object.with(key, peers)`](#objectwithkey-peers)
- [`object.without(key, peers)`](#objectwithoutkey-peers)
- [`object.rename(from, to, [options])`](#objectrenamefrom-to-options)
Expand Down Expand Up @@ -223,6 +224,7 @@
- [`object.with`](#objectwith)
- [`object.without`](#objectwithout)
- [`object.xor`](#objectxor)
- [`object.oxor`](#objectoxor)
- [`string.alphanum`](#stringalphanum-1)
- [`string.base64`](#stringbase64)
- [`string.base`](#stringbase)
Expand Down Expand Up @@ -1961,6 +1963,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.oxor`](#objectoxor)

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

Requires the presence of other keys whenever the specified key is present where:
Expand Down Expand Up @@ -3833,8 +3849,24 @@ The XOR condition between the properties you specified was not satisfied in that
{
key: string, // Last element of the path accessing the value, `undefined` if at the root
label: string, // Label if defined, otherwise it's the key
peers: Array<string>, // List of properties were none of it was set
peersWithLabels: Array<string> // List of labels for the properties were none of it was set
peers: Array<string>, // List of properties where none of it or too many of it was set
peersWithLabels: Array<string> // List of labels for the properties where none of it or too many of it was set
}
```
#### `object.oxor`
**Description**
The optional XOR condition between the properties you specified was not satisfied in that object.
**Context**
```ts
{
key: string, // Last element of the path accessing the value, `undefined` if at the root
label: string, // Label if defined, otherwise it's the key
peers: Array<string>, // List of properties where too many of it was set
peersWithLabels: Array<string> // List of labels for the properties where too many of it was set
}
```
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
169 changes: 128 additions & 41 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 @@ -2466,13 +2558,12 @@ describe('object', () => {
message: '"value" must contain at least one of [first, second]',
path: [],
type: 'object.missing',
context:
{
peers: ['a', 'b'],
peersWithLabels: ['first', 'second'],
label: 'value',
key: undefined
}
context: {
peers: ['a', 'b'],
peersWithLabels: ['first', 'second'],
label: 'value',
key: undefined
}
}]);
});

Expand Down Expand Up @@ -2517,13 +2608,12 @@ describe('object', () => {
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
}
context: {
peers: ['a', 'b.c'],
peersWithLabels: ['first', 'second'],
label: 'value',
key: undefined
}
}]);
});
});
Expand All @@ -2542,15 +2632,14 @@ describe('object', () => {
message: '"value" contains [first] without its required peers [second]',
path: [],
type: 'object.and',
context:
{
present: ['a'],
presentWithLabels: ['first'],
missing: ['b'],
missingWithLabels: ['second'],
label: 'value',
key: undefined
}
context: {
present: ['a'],
presentWithLabels: ['first'],
missing: ['b'],
missingWithLabels: ['second'],
label: 'value',
key: undefined
}
}]);
});

Expand Down Expand Up @@ -2597,15 +2686,14 @@ describe('object', () => {
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
}
context: {
present: ['a'],
presentWithLabels: ['first'],
missing: ['b.c'],
missingWithLabels: ['second'],
label: 'value',
key: undefined
}
}]);
});

Expand All @@ -2621,15 +2709,14 @@ describe('object', () => {
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
}
context: {
present: ['a'],
presentWithLabels: ['first'],
missing: ['c.d'],
missingWithLabels: ['c.d'],
label: 'value',
key: undefined
}
}]);
});
});
Expand Down

0 comments on commit 4653c3c

Please sign in to comment.