From c2c89e9e3af8651f2e9ee401579222daff1c354a Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Thu, 28 Apr 2022 12:56:50 +0100 Subject: [PATCH 01/15] feat: add validation for id references for requests only and objects for responses --- CONTRIBUTING.md | 2 +- src/classes/model-node.js | 4 + src/errors/validation-error-type.js | 2 + .../id-references-for-requests-rule-spec.js | 107 ++++++++++++++++++ .../core/id-references-for-requests-rule.js | 75 ++++++++++++ ...o-id-references-for-responses-rule-spec.js | 95 ++++++++++++++++ .../no-id-references-for-responses-rule.js | 74 ++++++++++++ 7 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 src/rules/core/id-references-for-requests-rule-spec.js create mode 100644 src/rules/core/id-references-for-requests-rule.js create mode 100644 src/rules/core/no-id-references-for-responses-rule-spec.js create mode 100644 src/rules/core/no-id-references-for-responses-rule.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c0e2545..4b2b2731 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -195,7 +195,7 @@ this.meta = { }; ``` -### validateModel and validateField +### `validateModel` and `validateField` Only one of these methods is expected to be implemented on each rule. diff --git a/src/classes/model-node.js b/src/classes/model-node.js index 45c3d29b..ef6be9db 100644 --- a/src/classes/model-node.js +++ b/src/classes/model-node.js @@ -181,4 +181,8 @@ const ModelNode = class { } }; +/** + * @typedef {ModelNode} ModelNodeType + */ + module.exports = ModelNode; diff --git a/src/errors/validation-error-type.js b/src/errors/validation-error-type.js index 79e9c46d..29b2b408 100644 --- a/src/errors/validation-error-type.js +++ b/src/errors/validation-error-type.js @@ -43,6 +43,8 @@ const ValidationErrorType = { BELOW_MIN_VALUE_INCLUSIVE: 'below_min_value_inclusive', VALUE_OUTWITH_CONSTRAINT: 'value_outwith_constraint', INVALID_ID: 'invalid_id', + FIELD_NOT_ID_REFERENCE: 'field_not_id_reference', + FIELD_SHOUlD_NOT_BE_ID_REFERENCE: 'field_should_not_be_id_reference', }; module.exports = Object.freeze(ValidationErrorType); diff --git a/src/rules/core/id-references-for-requests-rule-spec.js b/src/rules/core/id-references-for-requests-rule-spec.js new file mode 100644 index 00000000..aee2dace --- /dev/null +++ b/src/rules/core/id-references-for-requests-rule-spec.js @@ -0,0 +1,107 @@ +const Model = require('../../classes/model'); +const ModelNode = require('../../classes/model-node'); +const { IdReferencesForRequestsRule } = require('./id-references-for-requests-rule'); +const OptionsHelper = require('../../helpers/options'); +const ValidationErrorType = require('../../errors/validation-error-type'); +const ValidationErrorSeverity = require('../../errors/validation-error-severity'); + + +describe('IdReferencesForRequestsRule', () => { + const rule = new IdReferencesForRequestsRule(); + + const model = new Model({ + type: 'OrderItem', + }, 'latest'); + model.hasSpecification = true; + + it('should return a failure if a request object does not have `acceptedOffer` as a compact ID reference', async () => { + const options = new OptionsHelper({ validationMode: 'C1Request' }); + const data = { + '@context': 'https://openactive.io/', + '@type': 'OrderItem', + acceptedOffer: { + '@type': 'Offer', + '@id': 'https://example.com/offer/1', + }, + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(1); + expect(errors[0].type).toBe(ValidationErrorType.FIELD_NOT_ID_REFERENCE); + expect(errors[0].severity).toBe(ValidationErrorSeverity.FAILURE); + }); + it('should return a failure if a request object does not have `orderedItem` as a compact ID reference', async () => { + const options = new OptionsHelper({ validationMode: 'C1Request' }); + const data = { + '@context': 'https://openactive.io/', + '@type': 'OrderItem', + orderedItem: { + '@type': 'ScheduledSession', + '@id': 'https://example.com/session/1', + }, + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(1); + expect(errors[0].type).toBe(ValidationErrorType.FIELD_NOT_ID_REFERENCE); + expect(errors[0].severity).toBe(ValidationErrorSeverity.FAILURE); + }); + it('should return no errors if a response object has `acceptedOffer` as a data object', async () => { + const options = new OptionsHelper({ validationMode: 'C1Response' }); + const data = { + '@context': 'https://openactive.io/', + '@type': 'OrderItem', + acceptedOffer: { + '@type': 'ScheduledSession', + '@id': 'https://example.com/offer/1', + }, + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + expect(errors.length).toBe(0); + }); + it('should return no errors if a response object has `orderedItem` as a data object', async () => { + const options = new OptionsHelper({ validationMode: 'C1Response' }); + const data = { + '@context': 'https://openactive.io/', + '@type': 'OrderItem', + orderedItem: { + '@type': 'ScheduledSession', + '@id': 'https://example.com/session/1', + }, + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + expect(errors.length).toBe(0); + }); +}); diff --git a/src/rules/core/id-references-for-requests-rule.js b/src/rules/core/id-references-for-requests-rule.js new file mode 100644 index 00000000..ebc0ebc9 --- /dev/null +++ b/src/rules/core/id-references-for-requests-rule.js @@ -0,0 +1,75 @@ +const Rule = require('../rule'); +const PropertyHelper = require('../../helpers/property'); +const ValidationErrorCategory = require('../../errors/validation-error-category'); +const ValidationErrorSeverity = require('../../errors/validation-error-severity'); +const validationErrorType = require('../../errors/validation-error-type'); + +/** @typedef {import('../../classes/model-node').ModelNodeType} ModelNode */ + +class IdReferencesForRequestsRule extends Rule { + constructor(options) { + super(options); + this.targetValidationModes = [ + 'C1Request', + 'C2Request', + 'PRequest', + 'BRequest', + 'BOrderProposalRequest', + 'OrderPatch', + ]; + this.targetFields = { OrderItem: ['acceptedOffer', 'orderedItem'] }; + this.meta = { + name: 'IdReferencesForRequestsRule', + description: 'Validates that acceptedOffer and orderedItem are ID references and not objects for requests (C1, C2 etc)', + tests: { + default: { + description: `Raises a failure if the acceptedOffer or orderedItem within the OrderItem of a request is not a URL + (ie a reference to the object and not the object itself)`, + message: 'For requests, {{field}} must be an compact ID reference, not the object representing the data itself', + sampleValues: { + field: 'acceptedOffer', + }, + category: ValidationErrorCategory.CONFORMANCE, + severity: ValidationErrorSeverity.FAILURE, + type: validationErrorType.FIELD_NOT_ID_REFERENCE, + }, + }, + }; + } + + /** + * + * @param {ModelNode} node + * @param {string} field + */ + validateField(node, field) { + // Don't do this check for models that we don't actually have a spec for or for models that aren't JSON-LD + if (!node.model.hasSpecification || !node.model.isJsonLd) { + return []; + } + + const errors = []; + const fieldValue = node.getValue(field); + + if (typeof fieldValue !== 'string' || !PropertyHelper.isUrl(fieldValue)) { + errors.push( + this.createError( + 'default', + { + fieldValue, + path: node.getPath(field), + }, + { field }, + ), + ); + } else { + return []; + } + + return errors; + } +} + +module.exports = { + IdReferencesForRequestsRule, +}; diff --git a/src/rules/core/no-id-references-for-responses-rule-spec.js b/src/rules/core/no-id-references-for-responses-rule-spec.js new file mode 100644 index 00000000..050486ff --- /dev/null +++ b/src/rules/core/no-id-references-for-responses-rule-spec.js @@ -0,0 +1,95 @@ +const Model = require('../../classes/model'); +const ModelNode = require('../../classes/model-node'); +const OptionsHelper = require('../../helpers/options'); +const ValidationErrorType = require('../../errors/validation-error-type'); +const ValidationErrorSeverity = require('../../errors/validation-error-severity'); +const { NoIdReferencesForResponsesRule } = require('./no-id-references-for-responses-rule'); + + +describe('NoIdReferencesForResponsesRule', () => { + const rule = new NoIdReferencesForResponsesRule(); + + const model = new Model({ + type: 'OrderItem', + }, 'latest'); + model.hasSpecification = true; + + it('should return a failure if a response object does not have `acceptedOffer` as a data object', async () => { + const options = new OptionsHelper({ validationMode: 'C1Response' }); + const data = { + '@context': 'https://openactive.io/', + '@type': 'OrderItem', + acceptedOffer: 'https://example.com/offer/1', + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(1); + expect(errors[0].type).toBe(ValidationErrorType.FIELD_SHOUlD_NOT_BE_ID_REFERENCE); + expect(errors[0].severity).toBe(ValidationErrorSeverity.FAILURE); + }); + it('should return a failure if a response object does not have `orderedItem` as a data object', async () => { + const options = new OptionsHelper({ validationMode: 'C1Response' }); + const data = { + '@context': 'https://openactive.io/', + '@type': 'OrderItem', + orderedItem: 'https://example.com/session/1', + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(1); + expect(errors[0].type).toBe(ValidationErrorType.FIELD_SHOUlD_NOT_BE_ID_REFERENCE); + expect(errors[0].severity).toBe(ValidationErrorSeverity.FAILURE); + }); + it('should return no errors if a request object has `acceptedOffer` as a compact ID reference', async () => { + const options = new OptionsHelper({ validationMode: 'C1Request' }); + const data = { + '@context': 'https://openactive.io/', + '@type': 'OrderItem', + acceptedOffer: 'https://example.com/offer/1', + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + expect(errors.length).toBe(0); + }); + it('should return no errors if a request object has `orderedItem` as a compact ID reference', async () => { + const options = new OptionsHelper({ validationMode: 'C1Request' }); + const data = { + '@context': 'https://openactive.io/', + '@type': 'OrderItem', + orderedItem: 'https://example.com/session/1', + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + expect(errors.length).toBe(0); + }); +}); diff --git a/src/rules/core/no-id-references-for-responses-rule.js b/src/rules/core/no-id-references-for-responses-rule.js new file mode 100644 index 00000000..9a021a8e --- /dev/null +++ b/src/rules/core/no-id-references-for-responses-rule.js @@ -0,0 +1,74 @@ +const Rule = require('../rule'); +const ValidationErrorCategory = require('../../errors/validation-error-category'); +const ValidationErrorSeverity = require('../../errors/validation-error-severity'); +const validationErrorType = require('../../errors/validation-error-type'); + +/** @typedef {import('../../classes/model-node').ModelNodeType} ModelNode */ + +class NoIdReferencesForResponsesRule extends Rule { + constructor(options) { + super(options); + this.targetValidationModes = [ + 'C1Response', + 'C2Response', + 'PResponse', + 'BResponse', + 'OrdersFeed', + 'OrderStatus', + ]; + this.targetFields = { OrderItem: ['acceptedOffer', 'orderedItem'] }; + this.meta = { + name: 'NoIdReferencesForResponsesRule', + description: 'Validates that acceptedOffer and orderedItem are not ID references and not objects for responses (C1, C2 etc)', + tests: { + default: { + description: `Raises a failure if the acceptedOffer or orderedItem within the OrderItem of a response is a URL + (ie a reference to the object and not the object itself)`, + message: 'For responses, {{field}} must not be an compact ID reference, but the object representing the data itself', + sampleValues: { + field: 'acceptedOffer', + }, + category: ValidationErrorCategory.CONFORMANCE, + severity: ValidationErrorSeverity.FAILURE, + type: validationErrorType.FIELD_SHOUlD_NOT_BE_ID_REFERENCE, + }, + }, + }; + } + + /** + * + * @param {ModelNode} node + * @param {string} field + */ + validateField(node, field) { + // Don't do this check for models that we don't actually have a spec for or for models that aren't JSON-LD + if (!node.model.hasSpecification || !node.model.isJsonLd) { + return []; + } + + const errors = []; + const fieldValue = node.getValue(field); + + if (typeof fieldValue !== 'object') { + errors.push( + this.createError( + 'default', + { + fieldValue, + path: node.getPath(field), + }, + { field }, + ), + ); + } else { + return []; + } + + return errors; + } +} + +module.exports = { + NoIdReferencesForResponsesRule, +}; From 21f316e50e6b42980ba5b3b578525d3b08adf10a Mon Sep 17 00:00:00 2001 From: civsiv Date: Mon, 16 May 2022 10:55:41 +0100 Subject: [PATCH 02/15] Update src/rules/core/no-id-references-for-responses-rule.js Co-authored-by: Luke Winship --- src/rules/core/no-id-references-for-responses-rule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rules/core/no-id-references-for-responses-rule.js b/src/rules/core/no-id-references-for-responses-rule.js index 9a021a8e..b2c6262c 100644 --- a/src/rules/core/no-id-references-for-responses-rule.js +++ b/src/rules/core/no-id-references-for-responses-rule.js @@ -19,7 +19,7 @@ class NoIdReferencesForResponsesRule extends Rule { this.targetFields = { OrderItem: ['acceptedOffer', 'orderedItem'] }; this.meta = { name: 'NoIdReferencesForResponsesRule', - description: 'Validates that acceptedOffer and orderedItem are not ID references and not objects for responses (C1, C2 etc)', + description: 'Validates that acceptedOffer and orderedItem are not ID references and are objects for responses (C1, C2 etc)', tests: { default: { description: `Raises a failure if the acceptedOffer or orderedItem within the OrderItem of a response is a URL From 1c1947dd3b27a3b5bdaa907791f6518843b02cb6 Mon Sep 17 00:00:00 2001 From: civsiv Date: Mon, 16 May 2022 10:55:46 +0100 Subject: [PATCH 03/15] Update src/rules/core/id-references-for-requests-rule.js Co-authored-by: Luke Winship --- src/rules/core/id-references-for-requests-rule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rules/core/id-references-for-requests-rule.js b/src/rules/core/id-references-for-requests-rule.js index ebc0ebc9..922d2482 100644 --- a/src/rules/core/id-references-for-requests-rule.js +++ b/src/rules/core/id-references-for-requests-rule.js @@ -25,7 +25,7 @@ class IdReferencesForRequestsRule extends Rule { default: { description: `Raises a failure if the acceptedOffer or orderedItem within the OrderItem of a request is not a URL (ie a reference to the object and not the object itself)`, - message: 'For requests, {{field}} must be an compact ID reference, not the object representing the data itself', + message: 'For requests, {{field}} must be a compact ID reference, not the object representing the data itself', sampleValues: { field: 'acceptedOffer', }, From b3d0a0f025d981f1af7004cc8c7276c370e9a9e4 Mon Sep 17 00:00:00 2001 From: civsiv Date: Mon, 16 May 2022 10:55:51 +0100 Subject: [PATCH 04/15] Update src/rules/core/no-id-references-for-responses-rule.js Co-authored-by: Luke Winship --- src/rules/core/no-id-references-for-responses-rule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rules/core/no-id-references-for-responses-rule.js b/src/rules/core/no-id-references-for-responses-rule.js index b2c6262c..24fa0fe2 100644 --- a/src/rules/core/no-id-references-for-responses-rule.js +++ b/src/rules/core/no-id-references-for-responses-rule.js @@ -24,7 +24,7 @@ class NoIdReferencesForResponsesRule extends Rule { default: { description: `Raises a failure if the acceptedOffer or orderedItem within the OrderItem of a response is a URL (ie a reference to the object and not the object itself)`, - message: 'For responses, {{field}} must not be an compact ID reference, but the object representing the data itself', + message: 'For responses, {{field}} must not be a compact ID reference, but the object representing the data itself', sampleValues: { field: 'acceptedOffer', }, From edb157a3104aa8ccea974d18ce1002155a97c58f Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Mon, 16 May 2022 10:59:44 +0100 Subject: [PATCH 05/15] WIP: add targetRpdeKinds, but not implemented any tests that use it --- src/rules/rule.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/rules/rule.js b/src/rules/rule.js index fe9cd21f..bebac844 100644 --- a/src/rules/rule.js +++ b/src/rules/rule.js @@ -8,6 +8,9 @@ class Rule { this.targetModels = []; this.targetFields = {}; this.targetValidationModes = '*'; + // This option is used to target a specific RPDE feed. It is only read if the validation mode for this rule is + // either 'RPDEFeed' or 'BookableRPDEFeed'. + this.targetRpdeKinds = '*'; this.meta = { name: 'Rule', description: 'This is a base rule description that should be overridden.', @@ -18,7 +21,7 @@ class Rule { async validate(nodeToTest) { let errors = []; - if (!this.isValidationModeTargeted(nodeToTest.options.validationMode)) { + if (!this.isValidationModeTargeted(nodeToTest.options.validationMode, nodeToTest.options.rpdeKind)) { return errors; } @@ -126,11 +129,16 @@ class Rule { return false; } - isValidationModeTargeted(validationMode) { + isValidationModeTargeted(validationMode, rpdeKind) { if (this.targetValidationModes === '*') return true; if (this.targetValidationModes instanceof Array) { + if (validationMode === 'RPDEFeed' || validationMode === 'BookableRPDEFeed') { + if (this.targetRpdeKinds === '*') return true; + return this.targetRpdeKinds.includes(rpdeKind); + } + return this.targetValidationModes.includes(validationMode); } From 58238e64d5e6f51e290a9cc6e40bd6a3f047a84e Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Mon, 16 May 2022 11:02:51 +0100 Subject: [PATCH 06/15] replace typeof with lodash helper function --- package.json | 2 ++ src/rules/core/no-id-references-for-responses-rule.js | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index bc0a1628..32ddaaeb 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,12 @@ "license": "MIT", "dependencies": { "@openactive/data-models": "^2.0.219", + "@types/lodash": "^4.14.182", "axios": "^0.19.2", "currency-codes": "^1.5.1", "html-entities": "^1.3.1", "jsonpath": "^1.0.2", + "lodash": "^4.17.21", "moment": "^2.24.0", "rrule": "^2.6.2", "striptags": "^3.1.1", diff --git a/src/rules/core/no-id-references-for-responses-rule.js b/src/rules/core/no-id-references-for-responses-rule.js index 9a021a8e..a3cc93b4 100644 --- a/src/rules/core/no-id-references-for-responses-rule.js +++ b/src/rules/core/no-id-references-for-responses-rule.js @@ -1,3 +1,4 @@ +const _ = require('lodash'); const Rule = require('../rule'); const ValidationErrorCategory = require('../../errors/validation-error-category'); const ValidationErrorSeverity = require('../../errors/validation-error-severity'); @@ -50,7 +51,7 @@ class NoIdReferencesForResponsesRule extends Rule { const errors = []; const fieldValue = node.getValue(field); - if (typeof fieldValue !== 'object') { + if (!_.isPlainObject(fieldValue)) { errors.push( this.createError( 'default', From 97ca8a321af08246539f859d74dbb74b58b48f2c Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Tue, 7 Jun 2022 14:20:17 +0100 Subject: [PATCH 07/15] use referencedFields in data models for compact ID reference tests --- src/classes/model.js | 11 ++++++ .../id-references-for-requests-rule-spec.js | 25 +++++++++++++ .../core/id-references-for-requests-rule.js | 36 +++++++++---------- 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/src/classes/model.js b/src/classes/model.js index 57ace1c2..f5f47a68 100644 --- a/src/classes/model.js +++ b/src/classes/model.js @@ -116,6 +116,17 @@ const Model = class { return this.data.shallNotInclude || []; } + getReferencedFields(validationMode, containingFieldName) { + const specificContextualImperativeConfiguration = this.getImperativeConfigurationWithContext(validationMode, containingFieldName); + const specificImperativeConfiguration = this.getImperativeConfiguration(validationMode); + + if (specificContextualImperativeConfiguration && specificContextualImperativeConfiguration.referencedFields) return specificContextualImperativeConfiguration.referencedFields; + + if (specificImperativeConfiguration && specificImperativeConfiguration.referencedFields) return specificImperativeConfiguration.referencedFields; + + return this.data.referencedFields || []; + } + hasRecommendedField(field) { return PropertyHelper.arrayHasField(this.recommendedFields, field, this.version); } diff --git a/src/rules/core/id-references-for-requests-rule-spec.js b/src/rules/core/id-references-for-requests-rule-spec.js index aee2dace..7da34674 100644 --- a/src/rules/core/id-references-for-requests-rule-spec.js +++ b/src/rules/core/id-references-for-requests-rule-spec.js @@ -11,6 +11,10 @@ describe('IdReferencesForRequestsRule', () => { const model = new Model({ type: 'OrderItem', + referencedFields: [ + 'orderedItem', + 'acceptedOffer', + ], }, 'latest'); model.hasSpecification = true; @@ -23,6 +27,7 @@ describe('IdReferencesForRequestsRule', () => { '@type': 'Offer', '@id': 'https://example.com/offer/1', }, + orderedItem: 'https://example.com/item/2', }; const nodeToTest = new ModelNode( @@ -47,6 +52,7 @@ describe('IdReferencesForRequestsRule', () => { '@type': 'ScheduledSession', '@id': 'https://example.com/session/1', }, + acceptedOffer: 'https://example.com/offer/1', }; const nodeToTest = new ModelNode( @@ -94,6 +100,25 @@ describe('IdReferencesForRequestsRule', () => { }, }; + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + expect(errors.length).toBe(0); + }); + it('should return no errors if a request object has `orderedItem` as a compact ID reference', async () => { + const options = new OptionsHelper({ validationMode: 'C1Request' }); + const data = { + '@context': 'https://openactive.io/', + '@type': 'OrderItem', + orderedItem: 'https://example.com/session/1', + acceptedOffer: 'https://example.com/offer/1', + }; + const nodeToTest = new ModelNode( '$', data, diff --git a/src/rules/core/id-references-for-requests-rule.js b/src/rules/core/id-references-for-requests-rule.js index 922d2482..3d4380e2 100644 --- a/src/rules/core/id-references-for-requests-rule.js +++ b/src/rules/core/id-references-for-requests-rule.js @@ -17,7 +17,7 @@ class IdReferencesForRequestsRule extends Rule { 'BOrderProposalRequest', 'OrderPatch', ]; - this.targetFields = { OrderItem: ['acceptedOffer', 'orderedItem'] }; + this.targetModels = '*'; this.meta = { name: 'IdReferencesForRequestsRule', description: 'Validates that acceptedOffer and orderedItem are ID references and not objects for requests (C1, C2 etc)', @@ -38,34 +38,34 @@ class IdReferencesForRequestsRule extends Rule { } /** - * * @param {ModelNode} node - * @param {string} field */ - validateField(node, field) { + validateModel(node) { // Don't do this check for models that we don't actually have a spec for or for models that aren't JSON-LD if (!node.model.hasSpecification || !node.model.isJsonLd) { return []; } const errors = []; - const fieldValue = node.getValue(field); + const referencedFields = node.model.getReferencedFields(node.options.validationMode, node.name); + for (const field of referencedFields) { + const fieldValue = node.getValue(field); - if (typeof fieldValue !== 'string' || !PropertyHelper.isUrl(fieldValue)) { - errors.push( - this.createError( - 'default', - { - fieldValue, - path: node.getPath(field), - }, - { field }, - ), - ); - } else { - return []; + if (typeof fieldValue !== 'string' || !PropertyHelper.isUrl(fieldValue)) { + errors.push( + this.createError( + 'default', + { + fieldValue, + path: node.getPath(field), + }, + { referencedField: field }, + ), + ); + } } + return errors; } } From ad93a962d6f0a56cf76f9314cd313fcb34408f45 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Thu, 9 Jun 2022 15:16:05 +0100 Subject: [PATCH 08/15] add id reference tests for feeds --- src/classes/model.js | 18 +- src/helpers/options.js | 4 + .../core/id-references-for-feeds-rule-spec.js | 157 ++++++++++++++++++ .../core/id-references-for-feeds-rule.js | 76 +++++++++ .../core/id-references-for-requests-rule.js | 1 - src/validate-spec.js | 2 +- 6 files changed, 247 insertions(+), 11 deletions(-) create mode 100644 src/rules/core/id-references-for-feeds-rule-spec.js create mode 100644 src/rules/core/id-references-for-feeds-rule.js diff --git a/src/classes/model.js b/src/classes/model.js index f5f47a68..f2d53eee 100644 --- a/src/classes/model.js +++ b/src/classes/model.js @@ -51,7 +51,7 @@ const Model = class { return this.imperativeConfiguration[imperativeConfigName]; } - getImperativeConfigurationWithContext(validationMode, containingFieldName) { + getImperativeConfigurationWithContext(validationMode, { containingFieldName, rpdeKind }) { if (!this.validationMode) return undefined; if (!this.imperativeConfigurationWithContext) return undefined; @@ -63,13 +63,13 @@ const Model = class { if (!contextualImperativeConfigs) return undefined; - const contextualImperativeConfig = contextualImperativeConfigs[containingFieldName]; + const fieldContextualImperativeConfig = contextualImperativeConfigs[containingFieldName]; - return contextualImperativeConfig; + return (!fieldContextualImperativeConfig) ? contextualImperativeConfigs[rpdeKind] : fieldContextualImperativeConfig; } getRequiredFields(validationMode, containingFieldName) { - const specificContextualImperativeConfiguration = this.getImperativeConfigurationWithContext(validationMode, containingFieldName); + const specificContextualImperativeConfiguration = this.getImperativeConfigurationWithContext(validationMode, { containingFieldName }); const specificImperativeConfiguration = this.getImperativeConfiguration(validationMode); if (specificContextualImperativeConfiguration && specificContextualImperativeConfiguration.requiredFields) return specificContextualImperativeConfiguration.requiredFields; @@ -84,7 +84,7 @@ const Model = class { } getRequiredOptions(validationMode, containingFieldName) { - const specificContextualImperativeConfiguration = this.getImperativeConfigurationWithContext(validationMode, containingFieldName); + const specificContextualImperativeConfiguration = this.getImperativeConfigurationWithContext(validationMode, { containingFieldName }); const specificImperativeConfiguration = this.getImperativeConfiguration(validationMode); if (specificContextualImperativeConfiguration && specificContextualImperativeConfiguration.requiredOptions) return specificContextualImperativeConfiguration.requiredOptions; @@ -95,7 +95,7 @@ const Model = class { } getRecommendedFields(validationMode, containingFieldName) { - const specificContextualImperativeConfiguration = this.getImperativeConfigurationWithContext(validationMode, containingFieldName); + const specificContextualImperativeConfiguration = this.getImperativeConfigurationWithContext(validationMode, { containingFieldName }); const specificImperativeConfiguration = this.getImperativeConfiguration(validationMode); if (specificContextualImperativeConfiguration && specificContextualImperativeConfiguration.recommendedFields) return specificContextualImperativeConfiguration.recommendedFields; @@ -106,7 +106,7 @@ const Model = class { } getShallNotIncludeFields(validationMode, containingFieldName) { - const specificContextualImperativeConfiguration = this.getImperativeConfigurationWithContext(validationMode, containingFieldName); + const specificContextualImperativeConfiguration = this.getImperativeConfigurationWithContext(validationMode, { containingFieldName }); const specificImperativeConfiguration = this.getImperativeConfiguration(validationMode); if (specificContextualImperativeConfiguration && specificContextualImperativeConfiguration.shallNotInclude) return specificContextualImperativeConfiguration.shallNotInclude; @@ -116,8 +116,8 @@ const Model = class { return this.data.shallNotInclude || []; } - getReferencedFields(validationMode, containingFieldName) { - const specificContextualImperativeConfiguration = this.getImperativeConfigurationWithContext(validationMode, containingFieldName); + getReferencedFields(validationMode, { containingFieldName, rpdeKind }) { + const specificContextualImperativeConfiguration = this.getImperativeConfigurationWithContext(validationMode, { containingFieldName, rpdeKind }); const specificImperativeConfiguration = this.getImperativeConfiguration(validationMode); if (specificContextualImperativeConfiguration && specificContextualImperativeConfiguration.referencedFields) return specificContextualImperativeConfiguration.referencedFields; diff --git a/src/helpers/options.js b/src/helpers/options.js index d3826cb9..fe905dde 100644 --- a/src/helpers/options.js +++ b/src/helpers/options.js @@ -30,6 +30,10 @@ const OptionsHelper = class { get validationMode() { return this.options.validationMode || 'RPDEFeed'; } + + get rpdeKind() { + return this.options.rpdeKind || null; + } }; module.exports = OptionsHelper; diff --git a/src/rules/core/id-references-for-feeds-rule-spec.js b/src/rules/core/id-references-for-feeds-rule-spec.js new file mode 100644 index 00000000..05ae7267 --- /dev/null +++ b/src/rules/core/id-references-for-feeds-rule-spec.js @@ -0,0 +1,157 @@ +const Model = require('../../classes/model'); +const ModelNode = require('../../classes/model-node'); +const OptionsHelper = require('../../helpers/options'); +const { IdReferencesForFeedsRule } = require('./id-references-for-feeds-rule'); +const ValidationErrorType = require('../../errors/validation-error-type'); +const ValidationErrorSeverity = require('../../errors/validation-error-severity'); + +describe('IdReferencesForFeedsRule', () => { + const rule = new IdReferencesForFeedsRule(); + + describe('for kind FacilityUse/Slot or IndividualFacilityUse/Slot feeds', () => { + const model = new Model({ + type: 'Slot', + validationMode: { + RPDEFeed: 'feed', + BookableRPDEFeed: 'feed', + }, + rpdeKind: [ + 'FacilityUse/Slot', + 'IndividualFacilityUse/Slot', + ], + imperativeConfigurationWithContext: { + feed: { + 'FacilityUse/Slot': { + referencedFields: [ + 'facilityUse', + ], + }, + 'IndividualFacilityUse/Slot': { + referencedFields: [ + 'facilityUse', + ], + }, + }, + }, + imperativeConfiguration: { + feed: {}, + }, + }, 'latest'); + model.hasSpecification = true; + it('should validate that facilityUse within the Slot is a ID reference, and not an object', async () => { + const options = new OptionsHelper({ validationMode: 'RPDEFeed', rpdeKind: 'FacilityUse/Slot' }); + + const data = { + '@context': 'https://openactive.io/', + '@type': 'Slot', + facilityUse: 'https://example.com/item/2', + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(0); + }); + it('should error when the facilityUse within the Slot is an object not an ID reference', async () => { + const options = new OptionsHelper({ validationMode: 'RPDEFeed', rpdeKind: 'FacilityUse/Slot' }); + + const data = { + '@context': 'https://openactive.io/', + '@type': 'Slot', + facilityUse: { + '@id': 'https://example.com/item/2', + }, + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(1); + expect(errors[0].type).toBe(ValidationErrorType.FIELD_NOT_ID_REFERENCE); + expect(errors[0].severity).toBe(ValidationErrorSeverity.FAILURE); + }); + }); + describe('for kind ScheduledSessions feeds', () => { + const model = new Model({ + type: 'ScheduledSession', + validationMode: { + RPDEFeed: 'feed', + BookableRPDEFeed: 'feed', + }, + rpdeKind: [ + 'ScheduledSession', + 'ScheduledSession.SessionSeries', + ], + imperativeConfigurationWithContext: { + feed: { + ScheduledSession: { + referencedFields: [ + 'superEvent', + ], + }, + }, + }, + imperativeConfiguration: { + feed: {}, + }, + }, 'latest'); + model.hasSpecification = true; + + it('should validate that superEvent within the ScheduledSession is a ID reference, and not an object', async () => { + const options = new OptionsHelper({ validationMode: 'RPDEFeed', rpdeKind: 'ScheduledSession' }); + + const data = { + '@context': 'https://openactive.io/', + '@type': 'ScheduledSession', + superEvent: 'https://example.com/item/2', + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(0); + }); + it('should error when superEvent within the ScheduledSession is an object not an ID reference', async () => { + const options = new OptionsHelper({ validationMode: 'RPDEFeed', rpdeKind: 'ScheduledSession' }); + + const data = { + '@context': 'https://openactive.io/', + '@type': 'ScheduledSession', + superEvent: { + '@id': 'https://example.com/item/2', + }, + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(1); + expect(errors[0].type).toBe(ValidationErrorType.FIELD_NOT_ID_REFERENCE); + expect(errors[0].severity).toBe(ValidationErrorSeverity.FAILURE); + }); + }); +}); diff --git a/src/rules/core/id-references-for-feeds-rule.js b/src/rules/core/id-references-for-feeds-rule.js new file mode 100644 index 00000000..eb51e3d4 --- /dev/null +++ b/src/rules/core/id-references-for-feeds-rule.js @@ -0,0 +1,76 @@ +const Rule = require('../rule'); +const PropertyHelper = require('../../helpers/property'); +const ValidationErrorCategory = require('../../errors/validation-error-category'); +const ValidationErrorSeverity = require('../../errors/validation-error-severity'); +const validationErrorType = require('../../errors/validation-error-type'); + +/** @typedef {import('../../classes/model-node').ModelNodeType} ModelNode */ + +class IdReferencesForFeedsRule extends Rule { + constructor(options) { + super(options); + this.targetValidationModes = [ + 'RPDEFeed', + 'BookableRPDEFeed', + ]; + this.targetRpdeKinds = [ + 'FacilityUse/Slot', + 'IndividualFacilityUse/Slot', + 'ScheduledSession', + ]; + this.targetModels = '*'; + this.meta = { + name: 'IdReferencesForFeedsRule', + description: 'Validates that certain properties in the specified feeds are an ID reference and not objects', + tests: { + default: { + description: `Raises a failure if properties within the data object in a RPDE Feed is not an ID reference + (ie a reference to the object and not the object itself)`, + message: 'For {{rpdeKind}} feeds, {{field}} must be an compact ID reference, not the object representing the data itself', + sampleValues: { + feed: 'FacilityUse/Slot', + field: 'facilityUse', + }, + category: ValidationErrorCategory.CONFORMANCE, + severity: ValidationErrorSeverity.FAILURE, + type: validationErrorType.FIELD_NOT_ID_REFERENCE, + }, + }, + }; + } + + /** + * @param {ModelNode} node + */ + validateModel(node) { + // Don't do this check for models that we don't actually have a spec for or for models that aren't JSON-LD + if (!node.model.hasSpecification || !node.model.isJsonLd) { + return []; + } + + const errors = []; + const referencedFields = node.model.getReferencedFields(node.options.validationMode, { rpdeKind: node.options.rpdeKind }); + for (const field of referencedFields) { + const fieldValue = node.getValue(field); + + if (typeof fieldValue !== 'string' || !PropertyHelper.isUrl(fieldValue)) { + errors.push( + this.createError( + 'default', + { + fieldValue, + path: node.getPath(field), + }, + { referencedField: field }, + ), + ); + } + } + + return errors; + } +} + +module.exports = { + IdReferencesForFeedsRule, +}; diff --git a/src/rules/core/id-references-for-requests-rule.js b/src/rules/core/id-references-for-requests-rule.js index 3d4380e2..bf1c7db4 100644 --- a/src/rules/core/id-references-for-requests-rule.js +++ b/src/rules/core/id-references-for-requests-rule.js @@ -65,7 +65,6 @@ class IdReferencesForRequestsRule extends Rule { } } - return errors; } } diff --git a/src/validate-spec.js b/src/validate-spec.js index 98929027..37b21094 100644 --- a/src/validate-spec.js +++ b/src/validate-spec.js @@ -487,7 +487,7 @@ describe('validate', () => { expect(result[3].path).toBe('$.location'); }); - it('should cope with flexible model types', async () => { + fit('should cope with flexible model types', async () => { const place = { '@context': metaData.contextUrl, '@id': 'http://www.example.org/locations/gym', From da9f503efc0d878ea38b2c6610a59441258e4da9 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Thu, 9 Jun 2022 15:45:03 +0100 Subject: [PATCH 09/15] fix tests --- src/rules/rule.js | 9 +++++---- src/validate-spec.js | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/rules/rule.js b/src/rules/rule.js index bebac844..024a8a4b 100644 --- a/src/rules/rule.js +++ b/src/rules/rule.js @@ -10,7 +10,7 @@ class Rule { this.targetValidationModes = '*'; // This option is used to target a specific RPDE feed. It is only read if the validation mode for this rule is // either 'RPDEFeed' or 'BookableRPDEFeed'. - this.targetRpdeKinds = '*'; + this.targetRpdeKinds = null; this.meta = { name: 'Rule', description: 'This is a base rule description that should be overridden.', @@ -135,10 +135,11 @@ class Rule { if (this.targetValidationModes instanceof Array) { if (validationMode === 'RPDEFeed' || validationMode === 'BookableRPDEFeed') { - if (this.targetRpdeKinds === '*') return true; - return this.targetRpdeKinds.includes(rpdeKind); + if (this.targetRpdeKinds) { + if (this.targetRpdeKinds === '*') return true; + return this.targetRpdeKinds.includes(rpdeKind); + } } - return this.targetValidationModes.includes(validationMode); } diff --git a/src/validate-spec.js b/src/validate-spec.js index 37b21094..98929027 100644 --- a/src/validate-spec.js +++ b/src/validate-spec.js @@ -487,7 +487,7 @@ describe('validate', () => { expect(result[3].path).toBe('$.location'); }); - fit('should cope with flexible model types', async () => { + it('should cope with flexible model types', async () => { const place = { '@context': metaData.contextUrl, '@id': 'http://www.example.org/locations/gym', From cc59db27d38f1b731756daf1f9323630c7abe431 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Fri, 10 Jun 2022 15:17:56 +0100 Subject: [PATCH 10/15] tweak rules --- package.json | 2 +- .../core/id-references-for-feeds-rule-spec.js | 157 ------------------ .../core/id-references-for-feeds-rule.js | 76 --------- .../id-references-for-requests-rule-spec.js | 2 +- .../core/id-references-for-requests-rule.js | 4 +- src/rules/index.js | 1 + 6 files changed, 4 insertions(+), 238 deletions(-) delete mode 100644 src/rules/core/id-references-for-feeds-rule-spec.js delete mode 100644 src/rules/core/id-references-for-feeds-rule.js diff --git a/package.json b/package.json index 32ddaaeb..7f9d6e86 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "license": "MIT", "dependencies": { - "@openactive/data-models": "^2.0.219", + "@openactive/data-models": "github:openactive/data-models#2660e5d", "@types/lodash": "^4.14.182", "axios": "^0.19.2", "currency-codes": "^1.5.1", diff --git a/src/rules/core/id-references-for-feeds-rule-spec.js b/src/rules/core/id-references-for-feeds-rule-spec.js deleted file mode 100644 index 05ae7267..00000000 --- a/src/rules/core/id-references-for-feeds-rule-spec.js +++ /dev/null @@ -1,157 +0,0 @@ -const Model = require('../../classes/model'); -const ModelNode = require('../../classes/model-node'); -const OptionsHelper = require('../../helpers/options'); -const { IdReferencesForFeedsRule } = require('./id-references-for-feeds-rule'); -const ValidationErrorType = require('../../errors/validation-error-type'); -const ValidationErrorSeverity = require('../../errors/validation-error-severity'); - -describe('IdReferencesForFeedsRule', () => { - const rule = new IdReferencesForFeedsRule(); - - describe('for kind FacilityUse/Slot or IndividualFacilityUse/Slot feeds', () => { - const model = new Model({ - type: 'Slot', - validationMode: { - RPDEFeed: 'feed', - BookableRPDEFeed: 'feed', - }, - rpdeKind: [ - 'FacilityUse/Slot', - 'IndividualFacilityUse/Slot', - ], - imperativeConfigurationWithContext: { - feed: { - 'FacilityUse/Slot': { - referencedFields: [ - 'facilityUse', - ], - }, - 'IndividualFacilityUse/Slot': { - referencedFields: [ - 'facilityUse', - ], - }, - }, - }, - imperativeConfiguration: { - feed: {}, - }, - }, 'latest'); - model.hasSpecification = true; - it('should validate that facilityUse within the Slot is a ID reference, and not an object', async () => { - const options = new OptionsHelper({ validationMode: 'RPDEFeed', rpdeKind: 'FacilityUse/Slot' }); - - const data = { - '@context': 'https://openactive.io/', - '@type': 'Slot', - facilityUse: 'https://example.com/item/2', - }; - - const nodeToTest = new ModelNode( - '$', - data, - null, - model, - options, - ); - const errors = await rule.validate(nodeToTest); - - expect(errors.length).toBe(0); - }); - it('should error when the facilityUse within the Slot is an object not an ID reference', async () => { - const options = new OptionsHelper({ validationMode: 'RPDEFeed', rpdeKind: 'FacilityUse/Slot' }); - - const data = { - '@context': 'https://openactive.io/', - '@type': 'Slot', - facilityUse: { - '@id': 'https://example.com/item/2', - }, - }; - - const nodeToTest = new ModelNode( - '$', - data, - null, - model, - options, - ); - const errors = await rule.validate(nodeToTest); - - expect(errors.length).toBe(1); - expect(errors[0].type).toBe(ValidationErrorType.FIELD_NOT_ID_REFERENCE); - expect(errors[0].severity).toBe(ValidationErrorSeverity.FAILURE); - }); - }); - describe('for kind ScheduledSessions feeds', () => { - const model = new Model({ - type: 'ScheduledSession', - validationMode: { - RPDEFeed: 'feed', - BookableRPDEFeed: 'feed', - }, - rpdeKind: [ - 'ScheduledSession', - 'ScheduledSession.SessionSeries', - ], - imperativeConfigurationWithContext: { - feed: { - ScheduledSession: { - referencedFields: [ - 'superEvent', - ], - }, - }, - }, - imperativeConfiguration: { - feed: {}, - }, - }, 'latest'); - model.hasSpecification = true; - - it('should validate that superEvent within the ScheduledSession is a ID reference, and not an object', async () => { - const options = new OptionsHelper({ validationMode: 'RPDEFeed', rpdeKind: 'ScheduledSession' }); - - const data = { - '@context': 'https://openactive.io/', - '@type': 'ScheduledSession', - superEvent: 'https://example.com/item/2', - }; - - const nodeToTest = new ModelNode( - '$', - data, - null, - model, - options, - ); - const errors = await rule.validate(nodeToTest); - - expect(errors.length).toBe(0); - }); - it('should error when superEvent within the ScheduledSession is an object not an ID reference', async () => { - const options = new OptionsHelper({ validationMode: 'RPDEFeed', rpdeKind: 'ScheduledSession' }); - - const data = { - '@context': 'https://openactive.io/', - '@type': 'ScheduledSession', - superEvent: { - '@id': 'https://example.com/item/2', - }, - }; - - const nodeToTest = new ModelNode( - '$', - data, - null, - model, - options, - ); - const errors = await rule.validate(nodeToTest); - - expect(errors.length).toBe(1); - expect(errors[0].type).toBe(ValidationErrorType.FIELD_NOT_ID_REFERENCE); - expect(errors[0].severity).toBe(ValidationErrorSeverity.FAILURE); - }); - }); -}); diff --git a/src/rules/core/id-references-for-feeds-rule.js b/src/rules/core/id-references-for-feeds-rule.js deleted file mode 100644 index eb51e3d4..00000000 --- a/src/rules/core/id-references-for-feeds-rule.js +++ /dev/null @@ -1,76 +0,0 @@ -const Rule = require('../rule'); -const PropertyHelper = require('../../helpers/property'); -const ValidationErrorCategory = require('../../errors/validation-error-category'); -const ValidationErrorSeverity = require('../../errors/validation-error-severity'); -const validationErrorType = require('../../errors/validation-error-type'); - -/** @typedef {import('../../classes/model-node').ModelNodeType} ModelNode */ - -class IdReferencesForFeedsRule extends Rule { - constructor(options) { - super(options); - this.targetValidationModes = [ - 'RPDEFeed', - 'BookableRPDEFeed', - ]; - this.targetRpdeKinds = [ - 'FacilityUse/Slot', - 'IndividualFacilityUse/Slot', - 'ScheduledSession', - ]; - this.targetModels = '*'; - this.meta = { - name: 'IdReferencesForFeedsRule', - description: 'Validates that certain properties in the specified feeds are an ID reference and not objects', - tests: { - default: { - description: `Raises a failure if properties within the data object in a RPDE Feed is not an ID reference - (ie a reference to the object and not the object itself)`, - message: 'For {{rpdeKind}} feeds, {{field}} must be an compact ID reference, not the object representing the data itself', - sampleValues: { - feed: 'FacilityUse/Slot', - field: 'facilityUse', - }, - category: ValidationErrorCategory.CONFORMANCE, - severity: ValidationErrorSeverity.FAILURE, - type: validationErrorType.FIELD_NOT_ID_REFERENCE, - }, - }, - }; - } - - /** - * @param {ModelNode} node - */ - validateModel(node) { - // Don't do this check for models that we don't actually have a spec for or for models that aren't JSON-LD - if (!node.model.hasSpecification || !node.model.isJsonLd) { - return []; - } - - const errors = []; - const referencedFields = node.model.getReferencedFields(node.options.validationMode, { rpdeKind: node.options.rpdeKind }); - for (const field of referencedFields) { - const fieldValue = node.getValue(field); - - if (typeof fieldValue !== 'string' || !PropertyHelper.isUrl(fieldValue)) { - errors.push( - this.createError( - 'default', - { - fieldValue, - path: node.getPath(field), - }, - { referencedField: field }, - ), - ); - } - } - - return errors; - } -} - -module.exports = { - IdReferencesForFeedsRule, -}; diff --git a/src/rules/core/id-references-for-requests-rule-spec.js b/src/rules/core/id-references-for-requests-rule-spec.js index 7da34674..cb207dfd 100644 --- a/src/rules/core/id-references-for-requests-rule-spec.js +++ b/src/rules/core/id-references-for-requests-rule-spec.js @@ -1,6 +1,6 @@ const Model = require('../../classes/model'); const ModelNode = require('../../classes/model-node'); -const { IdReferencesForRequestsRule } = require('./id-references-for-requests-rule'); +const IdReferencesForRequestsRule = require('./id-references-for-requests-rule'); const OptionsHelper = require('../../helpers/options'); const ValidationErrorType = require('../../errors/validation-error-type'); const ValidationErrorSeverity = require('../../errors/validation-error-severity'); diff --git a/src/rules/core/id-references-for-requests-rule.js b/src/rules/core/id-references-for-requests-rule.js index bf1c7db4..d0bbb5c6 100644 --- a/src/rules/core/id-references-for-requests-rule.js +++ b/src/rules/core/id-references-for-requests-rule.js @@ -69,6 +69,4 @@ class IdReferencesForRequestsRule extends Rule { } } -module.exports = { - IdReferencesForRequestsRule, -}; +module.exports = IdReferencesForRequestsRule; diff --git a/src/rules/index.js b/src/rules/index.js index a7d0e3b3..c902e413 100644 --- a/src/rules/index.js +++ b/src/rules/index.js @@ -23,6 +23,7 @@ module.exports = { require('./core/valueconstraint-rule'), require('./core/minvalueinclusive-rule'), require('./core/id-rule'), + require('./core/id-references-for-certain-feeds-rule'), // Formatting rules require('./format/duration-format-rule'), From e04f092a6e4f72cc03df4a5eb3a6496f81a58e05 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Fri, 10 Jun 2022 15:18:44 +0100 Subject: [PATCH 11/15] add rules --- ...-references-for-certain-feeds-rule-spec.js | 157 ++++++++++++++++++ .../id-references-for-certain-feeds-rule.js | 74 +++++++++ 2 files changed, 231 insertions(+) create mode 100644 src/rules/core/id-references-for-certain-feeds-rule-spec.js create mode 100644 src/rules/core/id-references-for-certain-feeds-rule.js diff --git a/src/rules/core/id-references-for-certain-feeds-rule-spec.js b/src/rules/core/id-references-for-certain-feeds-rule-spec.js new file mode 100644 index 00000000..e5a50595 --- /dev/null +++ b/src/rules/core/id-references-for-certain-feeds-rule-spec.js @@ -0,0 +1,157 @@ +const Model = require('../../classes/model'); +const ModelNode = require('../../classes/model-node'); +const OptionsHelper = require('../../helpers/options'); +const IdReferencesForCertainFeedsRule = require('./id-references-for-certain-feeds-rule'); +const ValidationErrorType = require('../../errors/validation-error-type'); +const ValidationErrorSeverity = require('../../errors/validation-error-severity'); + +describe('IdReferencesForCertainFeedsRule', () => { + const rule = new IdReferencesForCertainFeedsRule(); + + describe('for kind FacilityUse/Slot or IndividualFacilityUse/Slot feeds', () => { + const model = new Model({ + type: 'Slot', + validationMode: { + RPDEFeed: 'feed', + BookableRPDEFeed: 'feed', + }, + rpdeKind: [ + 'FacilityUse/Slot', + 'IndividualFacilityUse/Slot', + ], + imperativeConfigurationWithContext: { + feed: { + 'FacilityUse/Slot': { + referencedFields: [ + 'facilityUse', + ], + }, + 'IndividualFacilityUse/Slot': { + referencedFields: [ + 'facilityUse', + ], + }, + }, + }, + imperativeConfiguration: { + feed: {}, + }, + }, 'latest'); + model.hasSpecification = true; + it('should validate that facilityUse within the Slot is a ID reference, and not an object', async () => { + const options = new OptionsHelper({ validationMode: 'RPDEFeed', rpdeKind: 'FacilityUse/Slot' }); + + const data = { + '@context': 'https://openactive.io/', + '@type': 'Slot', + facilityUse: 'https://example.com/item/2', + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(0); + }); + it('should error when the facilityUse within the Slot is an object not an ID reference', async () => { + const options = new OptionsHelper({ validationMode: 'RPDEFeed', rpdeKind: 'FacilityUse/Slot' }); + + const data = { + '@context': 'https://openactive.io/', + '@type': 'Slot', + facilityUse: { + '@id': 'https://example.com/item/2', + }, + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(1); + expect(errors[0].type).toBe(ValidationErrorType.FIELD_NOT_ID_REFERENCE); + expect(errors[0].severity).toBe(ValidationErrorSeverity.FAILURE); + }); + }); + describe('for kind ScheduledSessions feeds', () => { + const model = new Model({ + type: 'ScheduledSession', + validationMode: { + RPDEFeed: 'feed', + BookableRPDEFeed: 'feed', + }, + rpdeKind: [ + 'ScheduledSession', + 'ScheduledSession.SessionSeries', + ], + imperativeConfigurationWithContext: { + feed: { + ScheduledSession: { + referencedFields: [ + 'superEvent', + ], + }, + }, + }, + imperativeConfiguration: { + feed: {}, + }, + }, 'latest'); + model.hasSpecification = true; + + it('should validate that superEvent within the ScheduledSession is a ID reference, and not an object', async () => { + const options = new OptionsHelper({ validationMode: 'RPDEFeed', rpdeKind: 'ScheduledSession' }); + + const data = { + '@context': 'https://openactive.io/', + '@type': 'ScheduledSession', + superEvent: 'https://example.com/item/2', + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(0); + }); + it('should error when superEvent within the ScheduledSession is an object not an ID reference', async () => { + const options = new OptionsHelper({ validationMode: 'RPDEFeed', rpdeKind: 'ScheduledSession' }); + + const data = { + '@context': 'https://openactive.io/', + '@type': 'ScheduledSession', + superEvent: { + '@id': 'https://example.com/item/2', + }, + }; + + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + options, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(1); + expect(errors[0].type).toBe(ValidationErrorType.FIELD_NOT_ID_REFERENCE); + expect(errors[0].severity).toBe(ValidationErrorSeverity.FAILURE); + }); + }); +}); diff --git a/src/rules/core/id-references-for-certain-feeds-rule.js b/src/rules/core/id-references-for-certain-feeds-rule.js new file mode 100644 index 00000000..4e25fd9d --- /dev/null +++ b/src/rules/core/id-references-for-certain-feeds-rule.js @@ -0,0 +1,74 @@ +const Rule = require('../rule'); +const PropertyHelper = require('../../helpers/property'); +const ValidationErrorCategory = require('../../errors/validation-error-category'); +const ValidationErrorSeverity = require('../../errors/validation-error-severity'); +const validationErrorType = require('../../errors/validation-error-type'); + +/** @typedef {import('../../classes/model-node').ModelNodeType} ModelNode */ + +class IdReferencesForCertainFeedsRule extends Rule { + constructor(options) { + super(options); + this.targetValidationModes = [ + 'RPDEFeed', + 'BookableRPDEFeed', + ]; + this.targetRpdeKinds = [ + 'FacilityUse/Slot', + 'IndividualFacilityUse/Slot', + 'ScheduledSession', + ]; + this.targetModels = '*'; + this.meta = { + name: 'IdReferencesForCertainFeedsRule', + description: 'Validates that certain properties in the specified feeds are an ID reference and not objects', + tests: { + default: { + description: `Raises a failure if properties within the data object in a RPDE Feed is not an ID reference + (ie a reference to the object and not the object itself)`, + message: 'For {{rpdeKind}} feeds, {{field}} must be an compact ID reference, not the object representing the data itself', + sampleValues: { + feed: 'FacilityUse/Slot', + field: 'facilityUse', + }, + category: ValidationErrorCategory.CONFORMANCE, + severity: ValidationErrorSeverity.FAILURE, + type: validationErrorType.FIELD_NOT_ID_REFERENCE, + }, + }, + }; + } + + /** + * @param {ModelNode} node + */ + validateModel(node) { + // Don't do this check for models that we don't actually have a spec for or for models that aren't JSON-LD + if (!node.model.hasSpecification || !node.model.isJsonLd) { + return []; + } + + const errors = []; + const referencedFields = node.model.getReferencedFields(node.options.validationMode, { rpdeKind: node.options.rpdeKind }); + for (const field of referencedFields) { + const fieldValue = node.getValue(field); + + if (typeof fieldValue !== 'string' || !PropertyHelper.isUrl(fieldValue)) { + errors.push( + this.createError( + 'default', + { + fieldValue, + path: node.getPath(field), + }, + { referencedField: field }, + ), + ); + } + } + + return errors; + } +} + +module.exports = IdReferencesForCertainFeedsRule; From 2abee72df951814494955bac8b520d9d2abe3b22 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Fri, 10 Jun 2022 15:20:20 +0100 Subject: [PATCH 12/15] add rules to index --- src/rules/core/id-references-for-requests-rule-spec.js | 2 +- src/rules/core/id-references-for-requests-rule.js | 4 +--- src/rules/core/no-id-references-for-responses-rule-spec.js | 2 +- src/rules/core/no-id-references-for-responses-rule.js | 4 +--- src/rules/index.js | 2 ++ 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/rules/core/id-references-for-requests-rule-spec.js b/src/rules/core/id-references-for-requests-rule-spec.js index 7da34674..cb207dfd 100644 --- a/src/rules/core/id-references-for-requests-rule-spec.js +++ b/src/rules/core/id-references-for-requests-rule-spec.js @@ -1,6 +1,6 @@ const Model = require('../../classes/model'); const ModelNode = require('../../classes/model-node'); -const { IdReferencesForRequestsRule } = require('./id-references-for-requests-rule'); +const IdReferencesForRequestsRule = require('./id-references-for-requests-rule'); const OptionsHelper = require('../../helpers/options'); const ValidationErrorType = require('../../errors/validation-error-type'); const ValidationErrorSeverity = require('../../errors/validation-error-severity'); diff --git a/src/rules/core/id-references-for-requests-rule.js b/src/rules/core/id-references-for-requests-rule.js index 3d4380e2..374607d6 100644 --- a/src/rules/core/id-references-for-requests-rule.js +++ b/src/rules/core/id-references-for-requests-rule.js @@ -70,6 +70,4 @@ class IdReferencesForRequestsRule extends Rule { } } -module.exports = { - IdReferencesForRequestsRule, -}; +module.exports = IdReferencesForRequestsRule; diff --git a/src/rules/core/no-id-references-for-responses-rule-spec.js b/src/rules/core/no-id-references-for-responses-rule-spec.js index 050486ff..bc7b57c6 100644 --- a/src/rules/core/no-id-references-for-responses-rule-spec.js +++ b/src/rules/core/no-id-references-for-responses-rule-spec.js @@ -3,7 +3,7 @@ const ModelNode = require('../../classes/model-node'); const OptionsHelper = require('../../helpers/options'); const ValidationErrorType = require('../../errors/validation-error-type'); const ValidationErrorSeverity = require('../../errors/validation-error-severity'); -const { NoIdReferencesForResponsesRule } = require('./no-id-references-for-responses-rule'); +const NoIdReferencesForResponsesRule = require('./no-id-references-for-responses-rule'); describe('NoIdReferencesForResponsesRule', () => { diff --git a/src/rules/core/no-id-references-for-responses-rule.js b/src/rules/core/no-id-references-for-responses-rule.js index c148b0c7..324aeda3 100644 --- a/src/rules/core/no-id-references-for-responses-rule.js +++ b/src/rules/core/no-id-references-for-responses-rule.js @@ -70,6 +70,4 @@ class NoIdReferencesForResponsesRule extends Rule { } } -module.exports = { - NoIdReferencesForResponsesRule, -}; +module.exports = NoIdReferencesForResponsesRule; diff --git a/src/rules/index.js b/src/rules/index.js index a7d0e3b3..fea8dfb6 100644 --- a/src/rules/index.js +++ b/src/rules/index.js @@ -23,6 +23,8 @@ module.exports = { require('./core/valueconstraint-rule'), require('./core/minvalueinclusive-rule'), require('./core/id-rule'), + require('./core/id-references-for-requests-rule'), + require('./core/no-id-references-for-responses-rule'), // Formatting rules require('./format/duration-format-rule'), From c00645162d6e2e01fe75974ea2ee9797e65bba6f Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Fri, 10 Jun 2022 16:08:53 +0100 Subject: [PATCH 13/15] add shallNotBeReferencedFields functionality --- src/classes/model.js | 11 ++++ ...o-id-references-for-responses-rule-spec.js | 58 +++++++++++++++++++ .../no-id-references-for-responses-rule.js | 35 ++++++----- 3 files changed, 86 insertions(+), 18 deletions(-) diff --git a/src/classes/model.js b/src/classes/model.js index f5f47a68..7fe2b64f 100644 --- a/src/classes/model.js +++ b/src/classes/model.js @@ -127,6 +127,17 @@ const Model = class { return this.data.referencedFields || []; } + getShallNotBeReferencedFields(validationMode, containingFieldName) { + const specificContextualImperativeConfiguration = this.getImperativeConfigurationWithContext(validationMode, containingFieldName); + const specificImperativeConfiguration = this.getImperativeConfiguration(validationMode); + + if (specificContextualImperativeConfiguration && specificContextualImperativeConfiguration.shallNotBeReferencedFields) return specificContextualImperativeConfiguration.shallNotBeReferencedFields; + + if (specificImperativeConfiguration && specificImperativeConfiguration.shallNotBeReferencedFields) return specificImperativeConfiguration.shallNotBeReferencedFields; + + return this.data.shallNotBeReferencedFields || []; + } + hasRecommendedField(field) { return PropertyHelper.arrayHasField(this.recommendedFields, field, this.version); } diff --git a/src/rules/core/no-id-references-for-responses-rule-spec.js b/src/rules/core/no-id-references-for-responses-rule-spec.js index bc7b57c6..ae41d45b 100644 --- a/src/rules/core/no-id-references-for-responses-rule-spec.js +++ b/src/rules/core/no-id-references-for-responses-rule-spec.js @@ -11,6 +11,58 @@ describe('NoIdReferencesForResponsesRule', () => { const model = new Model({ type: 'OrderItem', + validationMode: { + C1Request: 'request', + C1Response: 'Cresponse', + }, + imperativeConfiguration: { + request: { + requiredFields: [ + 'type', + 'acceptedOffer', + 'orderedItem', + 'position', + ], + recommendedFields: [], + shallNotInclude: [ + 'id', + 'orderItemStatus', + 'unitTaxSpecification', + 'accessCode', + 'error', + 'cancellationMessage', + 'customerNotice', + 'orderItemIntakeForm', + ], + requiredOptions: [], + referencedFields: [ + 'orderedItem', + 'acceptedOffer', + ], + }, + Cresponse: { + requiredFields: [ + 'type', + 'acceptedOffer', + 'orderedItem', + 'position', + ], + shallNotInclude: [ + 'id', + 'orderItemStatus', + 'cancellationMessage', + 'customerNotice', + 'accessCode', + 'accessPass', + 'error', + ], + requiredOptions: [], + shallNotBeReferencedFields: [ + 'orderedItem', + 'acceptedOffer', + ], + }, + }, }, 'latest'); model.hasSpecification = true; @@ -20,6 +72,9 @@ describe('NoIdReferencesForResponsesRule', () => { '@context': 'https://openactive.io/', '@type': 'OrderItem', acceptedOffer: 'https://example.com/offer/1', + orderedItem: { + '@id': 'https://example.com/item/1', + }, }; const nodeToTest = new ModelNode( @@ -41,6 +96,9 @@ describe('NoIdReferencesForResponsesRule', () => { '@context': 'https://openactive.io/', '@type': 'OrderItem', orderedItem: 'https://example.com/session/1', + acceptedOffer: { + '@id': 'https://example.com/offer/1', + }, }; const nodeToTest = new ModelNode( diff --git a/src/rules/core/no-id-references-for-responses-rule.js b/src/rules/core/no-id-references-for-responses-rule.js index 324aeda3..6366ba67 100644 --- a/src/rules/core/no-id-references-for-responses-rule.js +++ b/src/rules/core/no-id-references-for-responses-rule.js @@ -17,7 +17,7 @@ class NoIdReferencesForResponsesRule extends Rule { 'OrdersFeed', 'OrderStatus', ]; - this.targetFields = { OrderItem: ['acceptedOffer', 'orderedItem'] }; + this.targetModels = '*'; this.meta = { name: 'NoIdReferencesForResponsesRule', description: 'Validates that acceptedOffer and orderedItem are not ID references and are objects for responses (C1, C2 etc)', @@ -38,32 +38,31 @@ class NoIdReferencesForResponsesRule extends Rule { } /** - * * @param {ModelNode} node - * @param {string} field */ - validateField(node, field) { + validateModel(node) { // Don't do this check for models that we don't actually have a spec for or for models that aren't JSON-LD if (!node.model.hasSpecification || !node.model.isJsonLd) { return []; } const errors = []; - const fieldValue = node.getValue(field); + const shouldNotBeReferencedFields = node.model.getShallNotBeReferencedFields(node.options.validationMode, node.name); + for (const field of shouldNotBeReferencedFields) { + const fieldValue = node.getValue(field); - if (!_.isPlainObject(fieldValue)) { - errors.push( - this.createError( - 'default', - { - fieldValue, - path: node.getPath(field), - }, - { field }, - ), - ); - } else { - return []; + if (!_.isPlainObject(fieldValue)) { + errors.push( + this.createError( + 'default', + { + fieldValue, + path: node.getPath(field), + }, + { field }, + ), + ); + } } return errors; From 1cb989b9952c81d41cc308d48d5de29a2d3c03e0 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Mon, 13 Jun 2022 15:43:40 +0100 Subject: [PATCH 14/15] add rule to index.js --- src/classes/model.js | 6 +++--- src/rules/index.js | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/classes/model.js b/src/classes/model.js index 8c3ec626..0abd9da1 100644 --- a/src/classes/model.js +++ b/src/classes/model.js @@ -51,7 +51,7 @@ const Model = class { return this.imperativeConfiguration[imperativeConfigName]; } - getImperativeConfigurationWithContext(validationMode, { containingFieldName, rpdeKind }) { + getImperativeConfigurationWithContext(validationMode, { containingFieldName = null, rpdeKind = null }) { if (!this.validationMode) return undefined; if (!this.imperativeConfigurationWithContext) return undefined; @@ -116,7 +116,7 @@ const Model = class { return this.data.shallNotInclude || []; } - getReferencedFields(validationMode, { containingFieldName, rpdeKind }) { + getReferencedFields(validationMode, { containingFieldName = null, rpdeKind = null }) { const specificContextualImperativeConfiguration = this.getImperativeConfigurationWithContext(validationMode, { containingFieldName, rpdeKind }); const specificImperativeConfiguration = this.getImperativeConfiguration(validationMode); @@ -127,7 +127,7 @@ const Model = class { return this.data.referencedFields || []; } - getShallNotBeReferencedFields(validationMode, { containingFieldName, rpdeKind }) { + getShallNotBeReferencedFields(validationMode, { containingFieldName = null, rpdeKind = null }) { const specificContextualImperativeConfiguration = this.getImperativeConfigurationWithContext(validationMode, { containingFieldName, rpdeKind }); const specificImperativeConfiguration = this.getImperativeConfiguration(validationMode); diff --git a/src/rules/index.js b/src/rules/index.js index 0818d9e2..07e145b8 100644 --- a/src/rules/index.js +++ b/src/rules/index.js @@ -24,6 +24,7 @@ module.exports = { require('./core/minvalueinclusive-rule'), require('./core/id-rule'), require('./core/id-references-for-certain-feeds-rule'), + require('./core/no-id-references-for-certain-feeds-rule'), require('./core/id-references-for-requests-rule'), require('./core/no-id-references-for-responses-rule'), From b7a74677cae54e5fe2b392a6e528d58f0f364de3 Mon Sep 17 00:00:00 2001 From: Civ Sivakumaran Date: Tue, 14 Jun 2022 11:08:37 +0100 Subject: [PATCH 15/15] update rule meta typo --- src/rules/core/id-references-for-certain-feeds-rule.js | 2 +- src/rules/core/no-id-references-for-certain-feeds-rule.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rules/core/id-references-for-certain-feeds-rule.js b/src/rules/core/id-references-for-certain-feeds-rule.js index 4e25fd9d..c06cf7ce 100644 --- a/src/rules/core/id-references-for-certain-feeds-rule.js +++ b/src/rules/core/id-references-for-certain-feeds-rule.js @@ -28,7 +28,7 @@ class IdReferencesForCertainFeedsRule extends Rule { (ie a reference to the object and not the object itself)`, message: 'For {{rpdeKind}} feeds, {{field}} must be an compact ID reference, not the object representing the data itself', sampleValues: { - feed: 'FacilityUse/Slot', + rpdeKind: 'FacilityUse/Slot', field: 'facilityUse', }, category: ValidationErrorCategory.CONFORMANCE, diff --git a/src/rules/core/no-id-references-for-certain-feeds-rule.js b/src/rules/core/no-id-references-for-certain-feeds-rule.js index 536f2305..ed820aa0 100644 --- a/src/rules/core/no-id-references-for-certain-feeds-rule.js +++ b/src/rules/core/no-id-references-for-certain-feeds-rule.js @@ -26,7 +26,7 @@ class NoIdReferencesForCertainFeedsRule extends Rule { (ie a reference to the object and not the object itself)`, message: 'For {{rpdeKind}} feeds, {{field}} must be not an compact ID reference, but the object representing the data itself', sampleValues: { - feed: 'ScheduledSession.SessionSeries', + rpdeKind: 'ScheduledSession.SessionSeries', field: 'superEvent', }, category: ValidationErrorCategory.CONFORMANCE,