From 9fe086a58e78076a179ff864c0791ddd3e474bea Mon Sep 17 00:00:00 2001 From: Timo Hill Date: Thu, 5 Nov 2020 10:19:50 +0000 Subject: [PATCH] Adding rule to ensure repeatFrequency is accompanied by appropriate byDay/byWeek/byMonthDay values --- src/errors/validation-error-type.js | 1 + ...schedule-repetition-frequency-rule-spec.js | 158 ++++++++++++++++++ ...tent-schedule-repetition-frequency-rule.js | 105 ++++++++++++ 3 files changed, 264 insertions(+) create mode 100644 src/rules/data-quality/consistent-schedule-repetition-frequency-rule-spec.js create mode 100644 src/rules/data-quality/consistent-schedule-repetition-frequency-rule.js diff --git a/src/errors/validation-error-type.js b/src/errors/validation-error-type.js index 06cf05f..66e7f67 100644 --- a/src/errors/validation-error-type.js +++ b/src/errors/validation-error-type.js @@ -40,6 +40,7 @@ const ValidationErrorType = { TYPE_LIMITS_USE: 'type_limits_use', WRONG_BASE_TYPE: 'wrong_base_type', FIELD_NOT_ALLOWED: 'field_not_allowed', + REPEATFREQUENCY_MISALIGNED: 'repeatfrequency_misaligned', }; module.exports = Object.freeze(ValidationErrorType); diff --git a/src/rules/data-quality/consistent-schedule-repetition-frequency-rule-spec.js b/src/rules/data-quality/consistent-schedule-repetition-frequency-rule-spec.js new file mode 100644 index 0000000..f722970 --- /dev/null +++ b/src/rules/data-quality/consistent-schedule-repetition-frequency-rule-spec.js @@ -0,0 +1,158 @@ +const ConsistentScheduleRepetitionFrequencyRule = require('./consistent-schedule-repetition-frequency-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'); + + +/* +Ensures that the value given in repeatFrequency is matched by an appropriate byDay, byWeek, or byMonth attribute. Possible values for repeatFrequency are in the first instance P[\d]D, P[\d]W, P[\d]M. In the first instance the repeatFrequency should be paired with a byDay attribute, in the second, byWeek, etc. Note that this test checks for the bare presence of appropriate attributes; there is no semantic checking +*/ + +describe('ConsistentScheduleRepetitionFrequencyRule', () => { + const rule = new ConsistentScheduleRepetitionFrequencyRule(); + + const model = new Model({ + type: 'Schedule', + fields: { + repeatFrequency: { + fieldName: 'repeatFrequency', + requiredType: 'https://schema.org/Text', + }, + byDay: { + fieldName: 'byDay', + requiredType: 'ArrayOf#https://schema.org/DayOfWeek', + }, + byMonth: { + fieldName: 'byMonth', + requiredType: 'ArrayOf#https://schema.org/Integer', + }, + byMonthDay: { + fieldName: 'byMonthDay', + requiredType: 'ArrayOf#https://schema.org/Integer', + }, + }, + }, 'latest'); + + it('should target Schedule models', () => { + const isTargeted = rule.isModelTargeted(model); + expect(isTargeted).toBe(true); + }); + + it('should return an error when a repeatFrequency is malformed', async () => { + const data = { + type: 'Schedule', + repeatFrequency: 'P1Day', + }; + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + + const errors = await rule.validate(nodeToTest); + expect(errors.length).toBe(1); + }); + it('should return no error when a repeatFrequency is daily and no further specification is given', async () => { + const data = { + type: 'Schedule', + repeatFrequency: 'P1D', + }; + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + + const errors = await rule.validate(nodeToTest); + expect(errors.length).toBe(0); + }); + it('should return no error when a weekly repeatFrequency has a byDay attribute', async () => { + const data = { + type: 'Schedule', + repeatFrequency: 'P1W', + startDate: '2020-11-03T13:00:00', + byDay: ['Monday', 'Thursday'], + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + const errors = await rule.validate(nodeToTest); + expect(errors.length).toBe(0); + }); + it('should return no error when a monthly repeatFrequency has a byMonthDay attribute', async () => { + const data = { + type: 'Schedule', + repeatFrequency: 'P1M', + byMonthDay: [1, 14], + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + const errors = await rule.validate(nodeToTest); + expect(errors.length).toBe(0); + }); + it('should return an error when a daily repeatFrequency has a byMonth attribute', async () => { + const data = { + type: 'Schedule', + repeatFrequency: 'P1D', + byMonth: [1, 3, 5, 7, 9, 11], + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + const errors = await rule.validate(nodeToTest); + expect(errors.length).toBe(1); + }); + + it('should return an error when a weekly repeatFrequency has a byMonthDay attribute', async () => { + const data = { + type: 'Schedule', + repeatFrequency: 'P1W', + startDate: '2020-11-03T13:00:00', + byMonthDay: [1, 3], + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + const errors = await rule.validate(nodeToTest); + expect(errors.length).toBe(1); + }); + it('should return an error when more than one repetition frequency period attribute is specified', async () => { + const data = { + type: 'Schedule', + repeatFrequency: 'P1W', + byMonth: [1, 3, 5, 7, 9, 11], + byDay: ['Monday', 'Thursday'], + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + const errors = await rule.validate(nodeToTest); + expect(errors.length).toBe(1); + expect(errors[0].type).toBe(ValidationErrorType.REPEATFREQUENCY_MISALIGNED); + expect(errors[0].severity).toBe(ValidationErrorSeverity.FAILURE); + }); +}); diff --git a/src/rules/data-quality/consistent-schedule-repetition-frequency-rule.js b/src/rules/data-quality/consistent-schedule-repetition-frequency-rule.js new file mode 100644 index 0000000..dad1b20 --- /dev/null +++ b/src/rules/data-quality/consistent-schedule-repetition-frequency-rule.js @@ -0,0 +1,105 @@ +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 ConsistentScheduleRepetitionFrequencyRule extends Rule { + constructor(options) { + super(options); + this.targetModels = ['Event', 'CourseInstance', 'EventSeries', 'HeadlineEvent', 'ScheduledSession', 'SessionSeries', 'Schedule', 'Slot']; + this.meta = { + name: 'ConsistentScheduleRepetitionFrequencyRule', + description: 'Ensures that the repeatFrequency of a Schedule is aligned with the correct frequency specifier: e.g., weekly repetition with a day of the week, monthly repetition with a week specified.', + tests: { + default: { + message: 'repeatFrequency must align with byDay/byWeek/byMonthWeek values of Schedule.', + category: ValidationErrorCategory.DATA_QUALITY, + severity: ValidationErrorSeverity.FAILURE, + type: ValidationErrorType.REPEATFREQUENCY_MISALIGNED, + }, + norepfreq: { + message: 'Schedules must contain a repeatFrequency', + category: ValidationErrorCategory.DATA_QUALITY, + severity: ValidationErrorSeverity.FAILURE, + type: ValidationErrorType.REPEATFREQUENCY_MISALIGNED, + }, + badrepfreq: { + message: 'repeatFrequency must conform to ISO 8601 duration values (e.g. "P1W", "P4M", etc.).', + category: ValidationErrorCategory.DATA_QUALITY, + severity: ValidationErrorSeverity.FAILURE, + type: ValidationErrorType.REPEATFREQUENCY_MISALIGNED, + }, + dayerr: { + message: 'Daily repeat frequencies should not have any additional "byDay", "byMonth", or "byMonthDay" attributes.', + category: ValidationErrorCategory.DATA_QUALITY, + severity: ValidationErrorSeverity.FAILURE, + type: ValidationErrorType.REPEATFREQUENCY_MISALIGNED, + }, + weekerr: { + message: 'Weekly repeat frequencies need a "byDay" attribute, and no others.', + category: ValidationErrorCategory.DATA_QUALITY, + severity: ValidationErrorSeverity.FAILURE, + type: ValidationErrorType.REPEATFREQUENCY_MISALIGNED, + }, + montherr: { + message: 'Monthly repeat frequencies need a "byMonthDay" attribute, and no others.', + category: ValidationErrorCategory.DATA_QUALITY, + severity: ValidationErrorSeverity.FAILURE, + type: ValidationErrorType.REPEATFREQUENCY_MISALIGNED, + }, + }, + }; + } + + validateModel(node) { + let repeatFrequency = node.getValue('repeatFrequency'); + const byDay = node.getValue('byDay'); + const byMonth = node.getValue('byMonth'); + const byMonthDay = node.getValue('byMonthDay'); + const errors = []; + + if (typeof repeatFrequency === 'undefined') { + errors.push(this.createError('norepfreq', {}, { model: node.model.type })); + return errors; + } + // check frequency is valud + repeatFrequency = repeatFrequency.toLowerCase(); + const regexp = /^p\d(d|w|m)$/; + if (!regexp.test(repeatFrequency)) { + errors.push(this.createError('badrepfreq', {}, { model: node.model.type })); + } + + // if frequency is valid, simple parsing will work + const period = repeatFrequency.slice(-1); + + switch (period) { + case 'd': + if (byDay !== undefined || byMonth !== undefined || byMonthDay !== undefined) { + errors.push(this.createError('dayerr', {}, { model: node.model.type })); + } + break; + + case 'w': + if (byDay === undefined) { + errors.push(this.createError('weekerr', {}, { model: node.model.type })); + } else if (byMonth !== undefined) { + errors.push(this.createError('weekerr', {}, { model: node.model.type })); + } else if (byMonthDay !== undefined) { + errors.push(this.createError('weekerr', {}, { model: node.model.type })); + } + break; + case 'm': + if (byDay !== undefined) { + errors.push(this.createError('montherr', {}, { model: node.model.type })); + } else if (byMonth !== undefined) { + errors.push(this.createError('montherr', {}, { model: node.model.type })); + } else if (byMonthDay === undefined) { + errors.push(this.createError('montherr', {}, { model: node.model.type })); + } + break; + default: + break; + } + return errors; + } +};