Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SW3: Timebox rule creation #336

Merged
merged 11 commits into from
Oct 29, 2019
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/classes/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ const Model = class {
return fields || [];
}

getShallNotIncludeFields(validationMode) {
const specificImperativeConfiguration = this.getImperativeConfiguration(validationMode);
if (typeof specificImperativeConfiguration === 'object') {
const fields = specificImperativeConfiguration.shallNotInclude;
return fields || [];
}
return undefined; // there are no default shallNotInclude fields
}

hasRecommendedField(field) {
return PropertyHelper.arrayHasField(this.recommendedFields, field, this.version);
}
Expand Down
88 changes: 88 additions & 0 deletions src/rules/core/shall-not-include-fields-rule-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const ShallNotIncludeFieldsRule = require('./shall-not-include-fields-rule');
const Model = require('../../classes/model');
const ModelNode = require('../../classes/model-node');
const ValidationErrorType = require('../../errors/validation-error-type');
const ValidationErrorSeverity = require('../../errors/validation-error-severity');
const OptionsHelper = require('../../helpers/options');
const ValidationMode = require('../../helpers/validation-mode');

describe('ShallNotIncludeFieldsRule', () => {
const rule = new ShallNotIncludeFieldsRule();

const model = new Model({
type: 'Event',
validationMode: {
C1Request: 'request',
},
imperativeConfiguration: {
request: {
shallNotInclude: [
'duration',
],
},
},
fields: {
remainigAttendeeCapacity: {
fieldName: 'remainingAttendeeCapacity',
requiredType: 'https://schema.org/Integer',
},
},
}, 'latest');

describe('when in a validation mode with shallNotInclude setting', () => {
const options = new OptionsHelper({ validationMode: ValidationMode.C1Request });

it('should return no error when no shall not include fields part of data', () => {
const data = {
type: 'Event',
};

const nodeToTest = new ModelNode(
'$',
data,
null,
model,
options,
);
const errors = rule.validate(nodeToTest);
expect(errors.length).toBe(0);
});

it('should return no error when shall not include fields present in data', () => {
const data = {
type: 'Event',
duration: 1,
};

const nodeToTest = new ModelNode(
'$',
data,
null,
model,
options,
);
const errors = rule.validate(nodeToTest);
expect(errors.length).toBe(1);
expect(errors[0].type).toBe(ValidationErrorType.FIELD_NOT_ALLOWED_IN_SPEC);
expect(errors[0].severity).toBe(ValidationErrorSeverity.FAILURE);
});
});

describe('when no imperative config for validation mode', () => {
it('should not create errors', () => {
const data = {
type: 'Event',
duration: 1,
};

const nodeToTest = new ModelNode(
'$',
data,
null,
model,
);
const errors = rule.validate(nodeToTest);
expect(errors.length).toBe(0);
});
});
});
45 changes: 45 additions & 0 deletions src/rules/core/shall-not-include-fields-rule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const Rule = require('../rule');
const PropertyHelper = require('../../helpers/property');
const ValidationErrorType = require('../../errors/validation-error-type');
const ValidationErrorCategory = require('../../errors/validation-error-category');
const ValidationErrorSeverity = require('../../errors/validation-error-severity');

module.exports = class ShallNotIncludeFieldsRule extends Rule {
constructor(options) {
super(options);
this.targetFields = '*';
this.meta = {
name: 'ShallNotIncludeFieldsRule',
description: 'Validates that fields that there are no fields that should not be included',
tests: {
default: {
message: 'Cannot include fields that are listed in the ShallNodeInclude specification for the given validation mode',
category: ValidationErrorCategory.CONFORMANCE,
severity: ValidationErrorSeverity.FAILURE,
type: ValidationErrorType.FIELD_NOT_ALLOWED_IN_SPEC,
},
},
};
}

validateField(node, field) {
const errors = [];

const shallNots = node.model.getShallNotIncludeFields(node.options.validationMode);
if (typeof shallNots !== 'undefined') {
if (PropertyHelper.arrayHasField(shallNots, field, node.model.version)) {
errors.push(
this.createError(
'default',
{
value: node.getValue(field),
path: node.getPath(field),
},
),
);
}
}

return errors;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const AvailableChannelForPrepaymentRule = require('./available-channel-for-prepayment-rule');
const Model = require('../../classes/model');
const ModelNode = require('../../classes/model-node');
const ValidationErrorType = require('../../errors/validation-error-type');
const ValidationErrorSeverity = require('../../errors/validation-error-severity');

describe('AvailableChannelForPrepaymentRule', () => {
const rule = new AvailableChannelForPrepaymentRule();

const model = new Model({
type: 'Offer',
fields: {
availableChannel: {
fieldName: 'availableChannel',
requiredType: 'ArrayOf#https://openactive.io/AvailableChannelType',
},
prepayment: {
fieldName: 'prepayment',
requiredType: 'https://openactive.io/RequiredStatusType',
},
},
}, 'latest');

it('should target availableChannel field', () => {
const isTargeted = rule.isFieldTargeted(model, 'availableChannel');
expect(isTargeted).toBe(true);
});

it('should return no error when payment is not set', () => {
const data = {
type: 'Offer',
};

const nodeToTest = new ModelNode(
'$',
data,
null,
model,
);
const errors = rule.validate(nodeToTest);
expect(errors.length).toBe(0);
});

const invalidChannel = 'https://openactive.io/Optional';

const affectedPrepayments = ['https://openactive.io/Required', 'https://openactive.io/Optional'];
for (const prepayment of affectedPrepayments) {
describe(`when payment is ${prepayment}`, () => {
const validAvailableChannelsForPrepayment = ['https://openactive.io/OpenBookingPrepayment', 'https://openactive.io/TelephonePrepayment', 'https://openactive.io/OnlinePrepayment'];
for (const validChannel of validAvailableChannelsForPrepayment) {
it(`should return no error when availableChannel contains ${validChannel}`, () => {
const data = {
type: 'Offer',
prepayment,
availableChannel: [validChannel, invalidChannel],
};

const nodeToTest = new ModelNode(
'$',
data,
null,
model,
);
const errors = rule.validate(nodeToTest);
expect(errors.length).toBe(0);
});
}

it('should return an error when availableChannel does not contain a valid value', () => {
const data = {
type: 'Offer',
prepayment,
availableChannel: [invalidChannel],
};

const nodeToTest = new ModelNode(
'$',
data,
null,
model,
);
const errors = rule.validate(nodeToTest);
expect(errors.length).toBe(1);
expect(errors[0].severity).toBe(ValidationErrorSeverity.FAILURE);
expect(errors[0].type).toBe(ValidationErrorType.FIELD_NOT_IN_DEFINED_VALUES);
});
});
}
});
51 changes: 51 additions & 0 deletions src/rules/data-quality/available-channel-for-prepayment-rule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const Rule = require('../rule');
const ValidationErrorType = require('../../errors/validation-error-type');
const ValidationErrorCategory = require('../../errors/validation-error-category');
const ValidationErrorSeverity = require('../../errors/validation-error-severity');

module.exports = class AvailableChannelPrepaymentRule extends Rule {
constructor(options) {
super(options);
this.targetFields = { Offer: 'availableChannel' };
this.meta = {
name: 'AvailableChannelPrepaymentRule',
description: 'Validates if oa:prepayment is https://openactive.io/Required or https://openactive.io/Optional, then oa:availableChannel must contain at least one of https://openactive.io/OpenBookingPrepayment, https://openactive.io/TelephonePrepayment or https://openactive.io/OnlinePrepayment',
tests: {
default: {
message: 'The `{{availableChannel}}` does not contain a valid available channel when prepayment is required or optional.',
category: ValidationErrorCategory.CONFORMANCE,
severity: ValidationErrorSeverity.FAILURE,
type: ValidationErrorType.FIELD_NOT_IN_DEFINED_VALUES,
},
},
};
}

validateField(node, field) {
const errors = [];

const prepaymentValue = node.getValue('prepayment');
const availableChannels = node.getValue(field);

if (['https://openactive.io/Required', 'https://openactive.io/Optional'].includes(prepaymentValue)) {
const validAvailableChannels = ['https://openactive.io/OpenBookingPrepayment', 'https://openactive.io/TelephonePrepayment', 'https://openactive.io/OnlinePrepayment'];
const validAndPresentAvailableChannels = validAvailableChannels.filter(x => availableChannels.includes(x));
if (validAndPresentAvailableChannels.length === 0) {
errors.push(
this.createError(
'default',
{
value: availableChannels,
path: node.getPath('availableChannel'),
},
{
availableChannel: availableChannels,
},
),
);
}
}

return errors;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const EventRemainingAttendeeCapacityRule = require('./event-remaining-attendee-capacity-rule');
const Model = require('../../classes/model');
const ModelNode = require('../../classes/model-node');
const ValidationErrorType = require('../../errors/validation-error-type');
const ValidationErrorSeverity = require('../../errors/validation-error-severity');
const OptionsHelper = require('../../helpers/options');
const ValidationMode = require('../../helpers/validation-mode');

describe('EventRemainingAttendeeCapacityRule', () => {
const rule = new EventRemainingAttendeeCapacityRule();

const model = new Model({
type: 'Event',
fields: {
remainigAttendeeCapacity: {
fieldName: 'remainingAttendeeCapacity',
requiredType: 'https://schema.org/Integer',
},
},
}, 'latest');

it('should target remainigAttendeeCapacity fields', () => {
const isTargeted = rule.isFieldTargeted(model, 'remainigAttendeeCapacity');
expect(isTargeted).toBe(true);
});

describe('isValidationModeTargeted', () => {
const modesToTest = [ValidationMode.C1Request, ValidationMode.C1Response, ValidationMode.C2Request, ValidationMode.C2Response, ValidationMode.BRequest, ValidationMode.BResponse];

for (const mode of modesToTest) {
it(`should target ${mode}`, () => {
const isTargeted = rule.isValidationModeTargeted(mode);
expect(isTargeted).toBe(true);
});
}

it('should not target OpenData validation mode', () => {
const isTargeted = rule.isValidationModeTargeted(ValidationMode.OpenData);
expect(isTargeted).toBe(false);
});
});

describe('when in a booking mode like C1Request', () => {
const options = new OptionsHelper({ validationMode: ValidationMode.C1Request });

it('should return no error when remainingAttendeeCapacity is > 0', () => {
const data = {
type: 'Event',
remainigAttendeeCapacity: 1,
};

const nodeToTest = new ModelNode(
'$',
data,
null,
model,
options,
);
const errors = rule.validate(nodeToTest);
expect(errors.length).toBe(0);
});

it('should return no error when remainingAttendeeCapacity is < 0', () => {
const data = {
type: 'Event',
remainigAttendeeCapacity: -1,
};

const nodeToTest = new ModelNode(
'$',
data,
null,
model,
options,
);
const errors = rule.validate(nodeToTest);
expect(errors.length).toBe(1);
expect(errors[0].type).toBe(ValidationErrorType.FIELD_NOT_IN_DEFINED_VALUES);
expect(errors[0].severity).toBe(ValidationErrorSeverity.FAILURE);
});
});
});
Loading