-
Notifications
You must be signed in to change notification settings - Fork 1
365 - schedule validation #383
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
Open
Lathrisk
wants to merge
34
commits into
master
Choose a base branch
from
365/schedule-validation
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
41d1c19
data-quality: Add rule for scheduledTimezone in partialSchedule
b24a116
data-quality: Remove dangling comment
cedb66c
data-quality: Add rule for repeatCount is positive integer
cbe7f3a
package: Install moment-timezone
7f0725d
data-quality: Add rule to ensure timezone matches IANA list
a9dc16a
tests: rename import
26d0ffa
rules: Refactor repeatCount rule to use minValueInclusive
b1f4a3e
data-quality: Ensure Schedule templates (id & url) are valid UriTempl…
be6d53d
data-quality: schedule-templates: Add ensure {startDate} rule
b0ff0fb
data-quality: Add rule to check for recurrence data in schedule
8d6707a
data-quality: schedule-exception-dates: Add rule that checks if excep…
c397c08
data-helpers: refactor tests to use generateRRuleOptions
876e0f0
data-helpers: Remove old logging
5bc8b22
helpers: Separate concerns between helper files
0402176
data-quality: repeatCount rule now superseded by minValueInclusive ru…
76ea071
data-quality: schedules: Update messages and rule tests
e1312f8
rebase: Resolve duplicate
7d4b784
data-quality: recurrence rule: Update tests to include more information
d729577
data-quality: Update exception date rule test message
d9ec6f0
validation types: Correct case for consistency
4c7f8a9
data-quality: schedule: Exception dates error type update
4bb1e75
data-quality: schedule: Check scheduleEventType is a valid event subC…
95c782f
data-quality: schedule: Catch errors when the model or subclassgraph …
aff4abc
data-quality: schedules: Convert to UTC for RRule
a758210
helpers: Refactor date function calls
f867830
helpers: Ensure node uses UTC envvar for datetime functions
6823bf8
helpers: Set node process timezone
df598b8
data-quality: schedule: Use daylight savings shift for test
ac3566e
package.json: Fix rrule version
dc3ee61
helpers: Update datetime function
aba2103
schedule rule: spec: Simplify count
baf2035
Merge branch 'master' into 365/schedule-validation
nickevansuk 46f84cd
Merge branch 'master' into 365/schedule-validation
nickevansuk cc7a39a
Merge branch 'master' into 365/schedule-validation
nickevansuk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
const { DateTime } = require('luxon'); | ||
|
||
function getDateTime(ianaTimezone, dateString, timeString) { | ||
// Node pulls the timezone from the system on initialisation using the TZ environment variable. | ||
// We can change process.env.TZ to UTC. This will update the current Node process. | ||
process.env.TZ = 'UTC'; | ||
if (typeof dateString !== 'undefined' && typeof timeString !== 'undefined') { | ||
return DateTime.fromISO(`${dateString}T${timeString}`, { zone: ianaTimezone }).toJSDate(); | ||
} | ||
return undefined; | ||
} | ||
|
||
module.exports = getDateTime; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
const { parse } = require('iso8601-duration'); | ||
const { RRule } = require('rrule'); | ||
|
||
function getFrequency(repeatFrequency) { | ||
if (typeof repeatFrequency !== 'undefined') { | ||
const frequency = parse(repeatFrequency); | ||
|
||
if (frequency.hours !== 0) { | ||
return { freq: RRule.HOURLY, interval: frequency.hours }; | ||
} | ||
if (frequency.days !== 0) { | ||
return { freq: RRule.DAILY, interval: frequency.days }; | ||
} | ||
if (frequency.weeks !== 0) { | ||
return { freq: RRule.WEEKLY, interval: frequency.weeks }; | ||
} | ||
if (frequency.months !== 0) { | ||
return { freq: RRule.MONTHLY, interval: frequency.months }; | ||
} | ||
if (frequency.years !== 0) { | ||
return { freq: RRule.YEARLY, interval: frequency.years }; | ||
} | ||
} | ||
return { freq: undefined, interval: 0 }; | ||
} | ||
|
||
module.exports = getFrequency; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
const getDateTime = require('./datetime-helper'); | ||
|
||
function generateRRuleOptions(properties) { | ||
const dtStart = getDateTime('UTC', properties.startDate, properties.startTime); | ||
const dtEnd = getDateTime('UTC', properties.endDate, properties.endTime); | ||
|
||
const rruleOptions = {}; | ||
|
||
if (typeof properties.freq !== 'undefined') { | ||
rruleOptions.freq = properties.freq; | ||
} | ||
if (typeof properties.interval !== 'undefined') { | ||
rruleOptions.interval = properties.interval; | ||
} | ||
if (typeof dtStart !== 'undefined') { | ||
rruleOptions.dtstart = dtStart; | ||
} | ||
if (typeof dtEnd !== 'undefined') { | ||
rruleOptions.until = dtEnd; | ||
} | ||
if (typeof properties.byDay !== 'undefined') { | ||
rruleOptions.byweekday = properties.byDay; | ||
} | ||
if (typeof properties.byMonth !== 'undefined') { | ||
rruleOptions.bymonth = properties.byMonth; | ||
} | ||
if (typeof properties.byMonthDay !== 'undefined') { | ||
rruleOptions.bymonthday = properties.byMonthDay; | ||
} | ||
if (typeof properties.count !== 'undefined') { | ||
rruleOptions.count = properties.count; | ||
} | ||
if (typeof properties.scheduleTimezone !== 'undefined') { | ||
rruleOptions.tzid = properties.scheduleTimezone; | ||
} | ||
return rruleOptions; | ||
} | ||
|
||
module.exports = generateRRuleOptions; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
const getFrequency = require('./frequency-converter'); | ||
|
||
function getScheduleProperties(node) { | ||
const { freq, interval } = getFrequency(node.getValue('repeatFrequency')); | ||
const properties = { | ||
freq, | ||
interval, | ||
byDay: node.getValue('byDay'), | ||
byMonth: node.getValue('byMonth'), | ||
byMonthDay: node.getValue('byMonthDay'), | ||
startDate: node.getValue('startDate'), | ||
startTime: node.getValue('startTime'), | ||
endDate: node.getValue('endDate'), | ||
endTime: node.getValue('endTime'), | ||
count: node.getValue('count'), | ||
scheduleTimezone: node.getValue('scheduleTimezone'), | ||
exceptDate: node.getValue('exceptDate'), | ||
}; | ||
return properties; | ||
} | ||
|
||
module.exports = getScheduleProperties; |
131 changes: 131 additions & 0 deletions
131
src/rules/data-quality/schedule-contains-recurrence-data-rule-spec.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
const ValidRecurrenceRule = require('./schedule-contains-recurrence-data-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('ValidRecurrenceRule', () => { | ||
const rule = new ValidRecurrenceRule(); | ||
const model = new Model({ | ||
type: 'Schedule', | ||
fields: { | ||
repeatFrequency: { | ||
fieldname: 'byDay', | ||
requiredType: 'https://schema.org/Duration', | ||
}, | ||
byDay: { | ||
fieldname: 'byDay', | ||
requiredType: 'ArrayOf#https://schema.org/DayOfWeek', | ||
alternativeTypes: ['ArrayOf#https://schema.org/Text'], | ||
}, | ||
byMonth: { | ||
fieldname: 'byMonth', | ||
requiredType: 'https://schema.org/Integer', | ||
}, | ||
byMonthDay: { | ||
fieldname: 'byMonthDay', | ||
requiredType: 'https://schema.org/Integer', | ||
}, | ||
startDate: { | ||
fieldname: 'startDate', | ||
requiredType: 'https://schema.org/Date', | ||
}, | ||
EndDate: { | ||
fieldname: 'EndDate', | ||
requiredType: 'https://schema.org/Date', | ||
}, | ||
startTime: { | ||
fieldname: 'startTime', | ||
requiredType: 'https://schema.org/Time', | ||
}, | ||
EndTime: { | ||
fieldname: 'EndTime', | ||
requiredType: 'https://schema.org/Time', | ||
}, | ||
count: { | ||
fieldname: 'count', | ||
requiredType: 'https://schema.org/Integer', | ||
}, | ||
scheduleTimezone: { | ||
fieldName: 'scheduleTimezone', | ||
requiredType: 'https://schema.org/Text', | ||
}, | ||
}, | ||
}, 'latest'); | ||
|
||
it('should target Schedule models', () => { | ||
const isTargeted = rule.isModelTargeted(model); | ||
expect(isTargeted).toBe(true); | ||
}); | ||
|
||
it('should return errors when startDate is missing', async () => { | ||
const data = { | ||
'@type': 'Schedule', | ||
startTime: '08:30', | ||
endTime: '09:30', | ||
scheduleTimezone: 'Europe/London', | ||
}; | ||
|
||
const nodeToTest = new ModelNode( | ||
'$', | ||
data, | ||
null, | ||
model, | ||
); | ||
|
||
const errors = await rule.validate(nodeToTest); | ||
|
||
expect(errors.length).toBe(2); | ||
for (const error of errors) { | ||
expect(error.type).toBe(ValidationErrorType.MISSING_REQUIRED_FIELD); | ||
expect(error.severity).toBe(ValidationErrorSeverity.FAILURE); | ||
} | ||
}); | ||
|
||
it('should return errors when startTime is missing', async () => { | ||
const data = { | ||
'@type': 'Schedule', | ||
startDate: '2021-03-19', | ||
repeatFrequency: 'P1W', | ||
count: 1, | ||
scheduleTimezone: 'Europe/London', | ||
}; | ||
|
||
const nodeToTest = new ModelNode( | ||
'$', | ||
data, | ||
null, | ||
model, | ||
); | ||
|
||
const errors = await rule.validate(nodeToTest); | ||
|
||
expect(errors.length).toBe(2); | ||
for (const error of errors) { | ||
expect(error.type).toBe(ValidationErrorType.MISSING_REQUIRED_FIELD); | ||
expect(error.severity).toBe(ValidationErrorSeverity.FAILURE); | ||
} | ||
}); | ||
|
||
it('should not return errors when there are sufficent properties to build a valid recurrence rule', async () => { | ||
const data = { | ||
'@type': 'Schedule', | ||
startDate: '2021-03-19', | ||
startTime: '08:30', | ||
repeatFrequency: 'P1W', | ||
count: 1, | ||
scheduleTimezone: 'Europe/London', | ||
}; | ||
|
||
const nodeToTest = new ModelNode( | ||
'$', | ||
data, | ||
null, | ||
model, | ||
); | ||
|
||
const errors = await rule.validate(nodeToTest); | ||
|
||
expect(errors.length).toBe(0); | ||
}); | ||
}); |
94 changes: 94 additions & 0 deletions
94
src/rules/data-quality/schedule-contains-recurrence-data-rule.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
const { RRule } = require('rrule'); | ||
const Rule = require('../rule'); | ||
const generateRRuleOptions = require('../../helpers/rrule-options'); | ||
const getScheduleProperties = require('../../helpers/schedule-properties'); | ||
const ValidationErrorType = require('../../errors/validation-error-type'); | ||
const ValidationErrorCategory = require('../../errors/validation-error-category'); | ||
const ValidationErrorSeverity = require('../../errors/validation-error-severity'); | ||
|
||
module.exports = class ValidRecurrenceRule extends Rule { | ||
constructor(options) { | ||
super(options); | ||
this.targetModels = ['Schedule']; | ||
this.meta = { | ||
name: 'ValidRecurrenceRule', | ||
description: | ||
'Validates that the Schedule contains the correct information to generate a valid iCal recurrence rule.', | ||
tests: { | ||
matchingFirstEvent: { | ||
message: | ||
'The first event that is generated by the `Schedule` ({{firstEvent}}) does not match the `startDate` ({{startDate}}) and `startTime` ({{startTime}}).', | ||
sampleValues: { | ||
startTime: '08:30', | ||
startDate: '2021-03-19', | ||
firstEvent: '2021-03-20T09:40:00Z', | ||
}, | ||
category: ValidationErrorCategory.CONFORMANCE, | ||
severity: ValidationErrorSeverity.FAILURE, | ||
type: ValidationErrorType.MISSING_REQUIRED_FIELD, | ||
}, | ||
rruleCreation: { | ||
message: | ||
'There was an error generating the RRule from the data provided. Error: {{error}}', | ||
category: ValidationErrorCategory.CONFORMANCE, | ||
severity: ValidationErrorSeverity.FAILURE, | ||
type: ValidationErrorType.MISSING_REQUIRED_FIELD, | ||
}, | ||
dtStart: { | ||
message: | ||
'The recurrence rule must contain a `startDate`, `startTime`, and `scheduledTimezone` to generate the schedule.', | ||
sampleValues: { | ||
startTime: '08:30', | ||
startDate: '2021-03-19', | ||
scheduleTimezone: 'Europe/London', | ||
}, | ||
category: ValidationErrorCategory.CONFORMANCE, | ||
severity: ValidationErrorSeverity.FAILURE, | ||
type: ValidationErrorType.MISSING_REQUIRED_FIELD, | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
validateModel(node) { | ||
const errors = []; | ||
|
||
const properties = getScheduleProperties(node); | ||
const rruleOptions = generateRRuleOptions(properties); | ||
|
||
if (typeof properties.startDate === 'undefined' | ||
|| typeof properties.startTime === 'undefined' | ||
|| typeof properties.scheduleTimezone === 'undefined') { | ||
errors.push( | ||
this.createError('dtStart', { | ||
value: undefined, | ||
path: node, | ||
}), | ||
); | ||
} | ||
|
||
try { | ||
const rule = new RRule(rruleOptions); | ||
const firstEvent = rule.all()[0]; | ||
if (firstEvent.getTime() !== rruleOptions.dtstart.getTime()) { | ||
errors.push( | ||
this.createError('matchingFirstEvent', { | ||
startDate: properties.startDate, | ||
startTime: properties.startTime, | ||
firstEvent, | ||
path: node, | ||
}), | ||
); | ||
} | ||
} catch (error) { | ||
errors.push( | ||
this.createError('rruleCreation', { | ||
error, | ||
path: node, | ||
}), | ||
); | ||
} | ||
|
||
return errors; | ||
} | ||
}; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.