From 8d483855c50658368b59edbe4a43185dd2dd3c18 Mon Sep 17 00:00:00 2001 From: Jared Parnell Date: Wed, 24 Mar 2021 10:56:23 +0000 Subject: [PATCH] data-quality: Ensure Schedule templates (id & url) are valid UriTemplates --- src/classes/field.js | 4 + .../schedule-templates-are-valid-rule-spec.js | 127 ++++++++++++++++++ .../schedule-templates-are-valid-rule.js | 52 +++++++ 3 files changed, 183 insertions(+) create mode 100644 src/rules/data-quality/schedule-templates-are-valid-rule-spec.js create mode 100644 src/rules/data-quality/schedule-templates-are-valid-rule.js diff --git a/src/classes/field.js b/src/classes/field.js index f748518..f1e5294 100644 --- a/src/classes/field.js +++ b/src/classes/field.js @@ -59,6 +59,10 @@ const Field = class { return this.data.minValueInclusive; } + get valueConstraint() { + return this.data.valueConstraint; + } + get standard() { return this.data.standard; } diff --git a/src/rules/data-quality/schedule-templates-are-valid-rule-spec.js b/src/rules/data-quality/schedule-templates-are-valid-rule-spec.js new file mode 100644 index 0000000..afe1505 --- /dev/null +++ b/src/rules/data-quality/schedule-templates-are-valid-rule-spec.js @@ -0,0 +1,127 @@ +const ScheduleTemplatesValid = require('./schedule-templates-are-valid-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('ScheduleTemplatesValid', () => { + let model; + let rule; + + beforeEach(() => { + model = new Model({ + type: 'Schedule', + fields: { + idTemplate: { + fieldName: 'idTemplate', + sameAs: 'https://openactive.io/idTemplate', + requiredType: 'https://schema.org/Text', + example: 'https://api.example.org/session-series/123/{startDate}', + description: [ + 'An RFC6570 compliant URI template that can be used to generate a unique identifier (`@id`) for every event described by the schedule. This property is required if the data provider is supporting third-party booking via the Open Booking API, or providing complimentary individual `subEvent`s.', + ], + valueConstraint: 'UriTemplate', + }, + urlTemplate: { + fieldName: 'urlTemplate', + sameAs: 'https://schema.org/urlTemplate', + requiredType: 'https://schema.org/Text', + example: 'https://example.org/session-series/123/{startDate}', + description: [ + 'An RFC6570 compliant URI template that can be used to generate a unique `url` for every event described by the schedule. This property is required if the data provider wants to provide participants with a unique URL to book to attend an event.', + ], + valueConstraint: 'UriTemplate', + }, + }, + }, 'latest'); + rule = new ScheduleTemplatesValid(); + }); + + it('should target idTemplate and urlTemplate in Schedule model', () => { + let isTargeted = rule.isFieldTargeted(model, 'idTemplate'); + expect(isTargeted).toBe(true); + + isTargeted = rule.isFieldTargeted(model, 'urlTemplate'); + expect(isTargeted).toBe(true); + }); + + it('should return no errors if the urlTemplate is valid', async () => { + const data = { + '@type': 'Schedule', + urlTemplate: 'https://api.example.org/session-series/123/{startDate}', + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(0); + }); + + it('should return no errors if the idTemplate is valid', async () => { + const data = { + '@type': 'Schedule', + idTemplate: 'https://api.example.org/session-series/123/{startDate}', + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(0); + }); + + it('should return errors if the urlTemplate is not valid', async () => { + const data = { + '@type': 'Schedule', + urlTemplate: 'htts://api.example.org/session-series/123/', + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(1); + for (const error of errors) { + expect(error.type).toBe(ValidationErrorType.INVALID_FORMAT); + expect(error.severity).toBe(ValidationErrorSeverity.FAILURE); + } + }); + + it('should return errors if the idTemplate is not valid', async () => { + const data = { + '@type': 'Schedule', + idTemplate: 'htts://api.example.org/session-series/123/', + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(1); + for (const error of errors) { + expect(error.type).toBe(ValidationErrorType.INVALID_FORMAT); + expect(error.severity).toBe(ValidationErrorSeverity.FAILURE); + } + }); +}); diff --git a/src/rules/data-quality/schedule-templates-are-valid-rule.js b/src/rules/data-quality/schedule-templates-are-valid-rule.js new file mode 100644 index 0000000..55ca305 --- /dev/null +++ b/src/rules/data-quality/schedule-templates-are-valid-rule.js @@ -0,0 +1,52 @@ +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 ScheduleTemplatesValid extends Rule { + constructor(options) { + super(options); + this.targetFields = { Schedule: ['urlTemplate', 'idTemplate'] }; + this.meta = { + name: 'ScheduleTemplatesValid', + description: 'Validates that the urlTemplate is of the correct format', + tests: { + default: { + description: 'Validates that the @context url matches the correct scheme and subdomain (https://openactive.io).', + message: 'When referencing the OpenActive domain, you must start your URLs with https://openactive.io.', + category: ValidationErrorCategory.CONFORMANCE, + severity: ValidationErrorSeverity.FAILURE, + type: ValidationErrorType.INVALID_FORMAT, + }, + }, + }; + } + + validateField(node, field) { + const fieldObj = node.model.getField(field); + const fieldValue = node.getValue(field); + + if (typeof fieldValue !== 'string') { + return []; + } + + const errors = []; + + if (typeof fieldObj.valueConstraint !== 'undefined' + && (fieldObj.valueConstraint === 'UriTemplate' + && !PropertyHelper.isUrlTemplate(fieldValue))) { + errors.push( + this.createError( + 'default', + { + fieldValue, + path: node.getPath(field), + }, + ), + ); + } + + return errors; + } +};