Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
},
"license": "MIT",
"dependencies": {
"@openactive/data-models": "^2.0.219",
"@openactive/data-models": "^2.0.289",
"@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",
Expand Down
4 changes: 4 additions & 0 deletions src/classes/model-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,8 @@ const ModelNode = class {
}
};

/**
* @typedef {ModelNode} ModelNodeType
*/

module.exports = ModelNode;
22 changes: 22 additions & 0 deletions src/classes/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,28 @@ 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 || [];
}

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);
}
Expand Down
2 changes: 2 additions & 0 deletions src/errors/validation-error-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const ValidationErrorType = {
BELOW_MIN_VALUE_INCLUSIVE: 'below_min_value_inclusive',
VALUE_OUTWITH_CONSTRAINT: 'value_outwith_constraint',
INVALID_ID: 'invalid_id',
FIELD_MUST_BE_ID_REFERENCE: 'FIELD_MUST_BE_ID_REFERENCE',
FIELD_MUST_NOT_BE_ID_REFERENCE: 'FIELD_MUST_NOT_BE_ID_REFERENCE',
};

module.exports = Object.freeze(ValidationErrorType);
4 changes: 4 additions & 0 deletions src/rules/booking/booking-root-type-correct-rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ module.exports = class BookingRootTypeCorrectRule extends Rule {
this.targetValidationModes = [
'C1Request',
'C1Response',
'C1ResponseOrderItemError',
'C2Request',
'C2Response',
'C2ResponseOrderItemError',
'PRequest',
'PResponse',
'PResponseOrderItemError',
'BRequest',
'BOrderProposalRequest',
'BResponse',
'BResponseOrderItemError',
'OrderProposalPatch',
'OrderPatch',
'OrdersFeed',
Expand Down
4 changes: 4 additions & 0 deletions src/rules/consumer-notes/assume-age-range-rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ module.exports = class AssumeAgeRangeRule extends Rule {
'RPDEFeed',
'BookableRPDEFeed',
'C1Response',
'C1ResponseOrderItemError',
'C2Response',
'C2ResponseOrderItemError',
'PResponse',
'PResponseOrderItemError',
'BResponse',
'BResponseOrderItemError',
];
this.meta = {
name: 'AssumeAgeRangeRule',
Expand Down
4 changes: 4 additions & 0 deletions src/rules/consumer-notes/assume-event-status-rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ module.exports = class AssumeEventStatusRule extends Rule {
'RPDEFeed',
'BookableRPDEFeed',
'C1Response',
'C1ResponseOrderItemError',
'C2Response',
'C2ResponseOrderItemError',
'PResponse',
'PResponseOrderItemError',
'BResponse',
'BResponseOrderItemError',
];
this.meta = {
name: 'AssumeEventStatusRule',
Expand Down
4 changes: 4 additions & 0 deletions src/rules/consumer-notes/assume-no-gender-restriction-rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ module.exports = class AssumeNoGenderRestrictionRule extends Rule {
'RPDEFeed',
'BookableRPDEFeed',
'C1Response',
'C1ResponseOrderItemError',
'C2Response',
'C2ResponseOrderItemError',
'PResponse',
'PResponseOrderItemError',
'BResponse',
'BResponseOrderItemError',
];
this.meta = {
name: 'AssumeNoGenderRestrictionRule',
Expand Down
34 changes: 22 additions & 12 deletions src/rules/core/fields-correct-type-rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ module.exports = class FieldsCorrectTypeRule extends Rule {
},
singleType: {
description: 'Validates that a property conforms to a single type.',
message: 'Invalid type, expected {{expectedType}} but found {{foundType}}.{{examples}}',
message: 'Invalid type, expected {{expectedType}}{{idReferencingMessage}} but found {{foundType}}.{{examples}}',
sampleValues: {
expectedType: this.constructor.getHumanReadableType('https://schema.org/Text'),
foundType: this.constructor.getHumanReadableType('https://schema.org/Number'),
examples: this.constructor.makeExamples('property', ['https://schema.org/Text'], this.options.version),
examples: this.constructor.makeExamples('property', ['https://schema.org/Text'], this.options.version, true),
},
category: ValidationErrorCategory.CONFORMANCE,
severity: ValidationErrorSeverity.FAILURE,
Expand All @@ -37,7 +37,7 @@ module.exports = class FieldsCorrectTypeRule extends Rule {
sampleValues: {
expectedType: this.constructor.getHumanReadableType('LocationFeatureSpecification'),
foundType: this.constructor.getHumanReadableType('ChangingRooms'),
examples: this.constructor.makeExamples('property', ['LocationFeatureSpecification'], this.options.version),
examples: this.constructor.makeExamples('property', ['LocationFeatureSpecification'], this.options.version, true),
},
category: ValidationErrorCategory.CONFORMANCE,
severity: ValidationErrorSeverity.FAILURE,
Expand All @@ -49,19 +49,19 @@ module.exports = class FieldsCorrectTypeRule extends Rule {
sampleValues: {
expectedType: this.constructor.getHumanReadableType('LocationFeatureSpecification'),
foundTypes: this.constructor.makeExpectedTypeList(['ChangingRooms', 'GolfCourse']),
examples: this.constructor.makeExamples('property', ['LocationFeatureSpecification'], this.options.version),
examples: this.constructor.makeExamples('property', ['LocationFeatureSpecification'], this.options.version, true),
},
category: ValidationErrorCategory.CONFORMANCE,
severity: ValidationErrorSeverity.FAILURE,
type: ValidationErrorType.INVALID_TYPE,
},
multipleTypes: {
description: 'Validates that a property conforms one of a list of types.',
message: 'Invalid type, expected one of {{expectedTypes}} but found {{foundType}}.{{examples}}',
message: 'Invalid type, expected one of {{expectedTypes}}{{idReferencingMessage}} but found {{foundType}}.{{examples}}',
sampleValues: {
expectedTypes: this.constructor.makeExpectedTypeList(['https://schema.org/Text', 'ArrayOf#https://schema.org/Text', '#Concept', 'ArrayOf#Concept']),
foundType: this.constructor.getHumanReadableType('https://schema.org/Number'),
examples: this.constructor.makeExamples('property', ['https://schema.org/Text', 'ArrayOf#https://schema.org/Text', '#Concept', 'ArrayOf#Concept'], this.options.version),
examples: this.constructor.makeExamples('property', ['https://schema.org/Text', 'ArrayOf#https://schema.org/Text', '#Concept', 'ArrayOf#Concept'], this.options.version, true),
},
category: ValidationErrorCategory.CONFORMANCE,
severity: ValidationErrorSeverity.FAILURE,
Expand Down Expand Up @@ -109,6 +109,8 @@ module.exports = class FieldsCorrectTypeRule extends Rule {
return `[\`string${plural}\` containing the URL of a property](${type}) from the [OpenActive](https://openactive.io/ns) or [schema.org](https://schema.org/) vocabularies`;
case 'https://schema.org/URL':
return `[\`string${plural}\` containing a url](${type})`;
case 'https://openactive.io/IdReference':
return '[`@id` reference](https://permalink.openactive.io/data-model-validator/id-references)';
default:
return `\`${type.replace(/^#/, '')}\``;
}
Expand All @@ -125,7 +127,7 @@ module.exports = class FieldsCorrectTypeRule extends Rule {
}
const humanReadableType = this.getHumanReadableRawType(readableType, isArray);
let aOrAn = 'A';
if (isArray || humanReadableType.match(/^\[?`[aeiouAEIOU]/)) {
if (isArray || humanReadableType.match(/^\[?`[aeiouAEIOU`]/)) {
aOrAn = 'An';
}
const hint = `${aOrAn} ${isArray ? 'array of ' : ''}${humanReadableType} looks like this:`;
Expand Down Expand Up @@ -162,6 +164,9 @@ module.exports = class FieldsCorrectTypeRule extends Rule {
case 'https://schema.org/URL':
example = `${prefix}"https://www.example.org/"`;
break;
case 'https://openactive.io/IdReference':
example = `${prefix}"https://id.example.com/api/session-series/1402CBP20150217"`;
break;
default:
if (PropertyHelper.isEnum(readableType, version)) {
const allowedOptions = PropertyHelper.getEnumOptions(readableType, version);
Expand All @@ -185,7 +190,7 @@ module.exports = class FieldsCorrectTypeRule extends Rule {
return `${expectedTypes}</ul>`;
}

static makeExamples(property, types, version, renderedExample) {
static makeExamples(property, types, version, renderedExample, allowReferencing) {
let examples = '';
for (const type of types) {
examples = `${examples}\n\n${this.getHumanReadableExample(property, type, version)}`;
Expand All @@ -194,6 +199,7 @@ module.exports = class FieldsCorrectTypeRule extends Rule {
const hint = types.length > 1 ? 'A full example of the preferred approach looks like this:' : 'A full example looks like this:';
examples = `${examples}\n\n${hint}\n\n${renderedExample}`;
}
if (allowReferencing) examples = `${examples}\n\nA URI reference which matches the \`@id\` of another object may also be used in place of the object itself.${this.getHumanReadableExample(property, 'https://openactive.io/IdReference', version)}`;
return examples;
}

Expand Down Expand Up @@ -241,6 +247,8 @@ module.exports = class FieldsCorrectTypeRule extends Rule {
// Pass check if referencing via a URL that matches an @id elsewhere is allowed, and in use
|| (fieldObj.allowReferencing && typeof fieldValue === 'string' && PropertyHelper.isUrl(fieldValue));

const idReferencingMessage = fieldObj.allowReferencing ? ' or a reference URI to an `@id`' : '';

if (!checkPass) {
let testKey;
let messageValues = {};
Expand Down Expand Up @@ -280,22 +288,23 @@ module.exports = class FieldsCorrectTypeRule extends Rule {
messageValues = {
expectedType: this.constructor.getHumanReadableType(typeChecks[0]),
foundType: this.constructor.getHumanReadableType(notAllowed[0]),
examples: this.constructor.makeExamples(propName, typeChecks, node.options.version, fieldObj.getRenderedExample()),
examples: this.constructor.makeExamples(propName, typeChecks, node.options.version, fieldObj.getRenderedExample(), fieldObj.allowReferencing),
};
} else if (notAllowed.length > 1) {
testKey = 'singleTypeSubclassMultipleError';
messageValues = {
expectedType: this.constructor.getHumanReadableType(typeChecks[0]),
foundTypes: this.constructor.makeExpectedTypeList(notAllowed),
examples: this.constructor.makeExamples(propName, typeChecks, node.options.version, fieldObj.getRenderedExample()),
examples: this.constructor.makeExamples(propName, typeChecks, node.options.version, fieldObj.getRenderedExample(), fieldObj.allowReferencing),
};
}
} else {
testKey = 'singleType';
messageValues = {
expectedType: this.constructor.getHumanReadableType(typeChecks[0]),
foundType: this.constructor.getHumanReadableType(derivedType),
examples: this.constructor.makeExamples(propName, typeChecks, node.options.version, fieldObj.getRenderedExample()),
examples: this.constructor.makeExamples(propName, typeChecks, node.options.version, fieldObj.getRenderedExample(), fieldObj.allowReferencing),
idReferencingMessage,
};
}
} else {
Expand All @@ -304,7 +313,8 @@ module.exports = class FieldsCorrectTypeRule extends Rule {
messageValues = {
expectedTypes,
foundType: this.constructor.getHumanReadableType(derivedType),
examples: this.constructor.makeExamples(propName, typeChecks, node.options.version, fieldObj.getRenderedExample()),
examples: this.constructor.makeExamples(propName, typeChecks, node.options.version, fieldObj.getRenderedExample(), fieldObj.allowReferencing),
idReferencingMessage,
};
}
errors.push(
Expand Down
Loading