Skip to content
This repository has been archived by the owner on May 17, 2024. It is now read-only.

Adding mutually-exclusive fields functional validation #25

Merged
merged 2 commits into from Sep 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/functional-constraints/index.js
Expand Up @@ -12,7 +12,8 @@
*/
const checks = [
require('./searchOrCreateKeys'),
require('./deepNestedFields')
require('./deepNestedFields'),
require('./mutuallyExclusiveFields'),
];

const runFunctionalConstraints = (definition) => {
Expand Down
59 changes: 59 additions & 0 deletions lib/functional-constraints/mutuallyExclusiveFields.js
@@ -0,0 +1,59 @@
'use strict';

const _ = require('lodash');
const jsonschema = require('jsonschema');

// NOTE: While it would be possible to accomplish this with a solution like
// https://stackoverflow.com/questions/28162509/mutually-exclusive-property-groups#28172831
// it was harder to read and understand.

const incompatibleFields = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is simpler indeed than the stackoverflow :).

Would it be helpful if we added comments on why the combinations are not available (comments would suffice)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point, but screen estate is limited for the errors, there. Do you have any suggestions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I meant inside the code.

['children', 'list'], // This is actually a Feature Request (https://github.com/zapier/zapier-platform-cli/issues/115)
['children', 'dict'], // dict is ignored
['children', 'type'], // type is ignored
['children', 'placeholder'], // placeholder is ignored
['children', 'helpText'], // helpText is ignored
['children', 'default'], // default is ignored
['dict', 'list'], // Use only one or the other
['dynamic', 'dict'], // dict is ignored
['dynamic', 'choices'], // choices are ignored
];

const verifyIncompatibilities = (inputFields, path) => {
const errors = [];

_.each(inputFields, (inputField, index) => {
_.each(incompatibleFields, ([firstField, secondField]) => {
if (_.has(inputField, firstField) && _.has(inputField, secondField)) {
errors.push(new jsonschema.ValidationError(
`must not contain ${firstField} and ${secondField}, as they're mutually exclusive.`,
inputField,
'/FieldSchema',
`instance.${path}.inputFields[${index}]`,
'invalid',
'inputFields'
));
}
});
});

return errors;
};

const mutuallyExclusiveFields = (definition) => {
let errors = [];

_.each(['triggers', 'searches', 'creates'], (typeOf) => {
if (definition[typeOf]) {
_.each(definition[typeOf], (actionDef) => {
if (actionDef.operation && actionDef.operation.inputFields) {
errors = [...errors, ...verifyIncompatibilities(actionDef.operation.inputFields, `${typeOf}.${actionDef.key}`)];
}
});
}
});

return errors;
};

module.exports = mutuallyExclusiveFields;
184 changes: 184 additions & 0 deletions test/functional-constraints/mutuallyExclusiveFields.js
@@ -0,0 +1,184 @@
'use strict';

require('should');
const schema = require('../../schema');

describe('mutuallyExclusiveFields', () => {

it('should not error on fields not mutually exclusive', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
inputFields: [
{key: 'orderId', type: 'number'},
{
key: 'line_items',
children: [
{
key: 'product', type: 'string',
},
],
},
],
},
},
},
};

const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(0);
});

it('should error on fields that have children and list', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
inputFields: [
{key: 'orderId', type: 'number'},
{
key: 'line_items',
children: [
{
key: 'product',
},
],
list: true,
},
],
},
},
},
};

const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql('instance.creates.foo.inputFields[1] must not contain children and list, as they\'re mutually exclusive.');
});

it('should error on fields that have list and dict', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
inputFields: [
{key: 'orderId', type: 'number'},
{
key: 'line_items',
dict: true,
list: true,
},
],
},
},
},
};

const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql('instance.creates.foo.inputFields[1] must not contain dict and list, as they\'re mutually exclusive.');
});

it('should error on fields that have dynamic and dict', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
inputFields: [
{
key: 'orderId',
type: 'number',
dynamic: 'foo.id.number',
dict: true,
},
{
key: 'line_items',
list: true,
},
],
},
},
},
};

const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql('instance.creates.foo.inputFields[0] must not contain dynamic and dict, as they\'re mutually exclusive.');
});

it('should error on fields that have dynamic and choices', () => {
const definition = {
version: '1.0.0',
platformVersion: '1.0.0',
creates: {
foo: {
key: 'foo',
noun: 'Foo',
display: {
label: 'Create Foo',
description: 'Creates a...',
},
operation: {
perform: '$func$2$f$',
inputFields: [
{
key: 'orderId',
type: 'number',
dynamic: 'foo.id.number',
choices: {
uno: 1,
dos: 2
},
},
{
key: 'line_items',
list: true,
},
],
},
},
},
};

const results = schema.validateAppDefinition(definition);
results.errors.should.have.length(1);
results.errors[0].stack.should.eql('instance.creates.foo.inputFields[0] must not contain dynamic and choices, as they\'re mutually exclusive.');
});
});