@openactive/data-model-validator
Version:
A library to allow a developer to validate a JSON document against the OpenActive Modelling Opportunity Specification
289 lines (284 loc) • 11.4 kB
JavaScript
const DataModelHelper = require('../../helpers/data-model');
const Rule = require('../rule');
const PropertyHelper = require('../../helpers/property');
const JsonLoaderHelper = require('../../helpers/json-loader');
const ValidationErrorType = require('../../errors/validation-error-type');
const ValidationErrorCategory = require('../../errors/validation-error-category');
const ValidationErrorSeverity = require('../../errors/validation-error-severity');
module.exports = class ActivityInActivityListRule extends Rule {
constructor(options) {
super(options);
this.targetFields = {
Event: ['activity'],
FacilityUse: ['activity'],
IndividualFacilityUse: ['activity'],
CourseInstance: ['activity'],
EventSeries: ['activity'],
HeadlineEvent: ['activity'],
SessionSeries: ['activity'],
Course: ['activity'],
};
this.meta = {
name: 'ActivityInActivityListRule',
description: 'Validates that an activity is in the OpenActive activity list.',
tests: {
default: {
message: 'Activity `"{{activity}}"` could not be found in the OpenActive activity list.',
sampleValues: {
activity: 'Touch Football',
},
category: ValidationErrorCategory.DATA_QUALITY,
severity: ValidationErrorSeverity.FAILURE,
type: ValidationErrorType.ACTIVITY_NOT_IN_ACTIVITY_LIST,
},
noPrefLabelMatch: {
message: 'Activity `"{{activity}}"` was found in the activity list `"{{list}}"`, but the `"prefLabel"` did not match.\n\nThe correct `"prefLabel"` is `"{{correctPrefLabel}}"`.',
sampleValues: {
activity: 'https://openactive.io/activity-list#dc8b8b2b-0a83-403f-863a-4ec05ebb2410',
correctPrefLabel: 'Touch Rugby Union',
list: 'https://openactive.io/activity-list',
},
category: ValidationErrorCategory.DATA_QUALITY,
severity: ValidationErrorSeverity.WARNING,
type: ValidationErrorType.ACTIVITY_NOT_IN_ACTIVITY_LIST,
},
noIdMatch: {
message: 'Activity `"{{activity}}"` was found in the activity list `"{{list}}"`, but the `"@id"` did not match.\n\nThe correct `"@id"` is `"{{correctId}}"`.',
sampleValues: {
activity: 'Touch Rugby Union',
correctId: 'https://openactive.io/activity-list#dc8b8b2b-0a83-403f-863a-4ec05ebb2410',
list: 'https://openactive.io/activity-list',
},
category: ValidationErrorCategory.DATA_QUALITY,
severity: ValidationErrorSeverity.FAILURE,
type: ValidationErrorType.ACTIVITY_NOT_IN_ACTIVITY_LIST,
},
listErrorCode: {
message: 'Activity list `"{{list}}"` did not return a valid HTTP status. The server returned an error {{code}}.',
sampleValues: {
list: 'https://openactive.io/activity-list/invalid-list.jsonld',
code: 200,
},
category: ValidationErrorCategory.INTERNAL,
severity: ValidationErrorSeverity.FAILURE,
type: ValidationErrorType.FILE_NOT_FOUND,
},
listInvalid: {
message: 'Activity list `"{{list}}"` did not return a valid JSON response. Please check that it contains a JSON document in the format described in [the specification](https://openactive.io/modelling-opportunity-data/#describing-activity-lists-code-skos-conceptscheme-code-and-physical-activity-code-skos-concept-code-).',
sampleValues: {
list: 'https://openactive.io/activity-list',
},
category: ValidationErrorCategory.INTERNAL,
severity: ValidationErrorSeverity.FAILURE,
type: ValidationErrorType.FILE_NOT_FOUND,
},
upgradeActivityList: {
message: 'URL `"https://openactive.io/activity-list"` should now be used in the `"inScheme"` property to reference the OpenActive Activity list rather than `"{{list}}"`.',
sampleValues: {
list: 'https://www.openactive.io/activity-list/activity-list.jsonld',
},
category: ValidationErrorCategory.DATA_QUALITY,
severity: ValidationErrorSeverity.FAILURE,
type: ValidationErrorType.FIELD_NOT_IN_DEFINED_VALUES,
},
useOfficialActivityList: {
message: 'To ensure that your data gets used by the largest number of apps and websites, you must align your activities with the official [OpenActive activity list](https://developer.openactive.io/publishing-data/activity-list-references).',
category: ValidationErrorCategory.DATA_QUALITY,
severity: ValidationErrorSeverity.FAILURE,
type: ValidationErrorType.USE_OFFICIAL_ACTIVITY_LIST,
},
},
};
}
async validateField(node, field) {
const fieldValue = node.getValue(field);
if (typeof fieldValue === 'undefined') {
return [];
}
const upgradeActivityLists = [
'https://www.openactive.io/activity-list/activity-list.jsonld',
'https://openactive.io/activity-list/activity-list.jsonld',
'http://www.openactive.io/activity-list/activity-list.jsonld',
'http://openactive.io/activity-list/activity-list.jsonld',
'https://www.openactive.io/activity-list/',
'https://openactive.io/activity-list/',
'http://www.openactive.io/activity-list/',
'http://openactive.io/activity-list/',
'https://www.openactive.io/activity-list',
'http://www.openactive.io/activity-list',
'http://openactive.io/activity-list',
];
const errors = [];
let found = false;
let idMatch = false;
let prefLabelMatch = false;
let correctId;
let correctPrefLabel;
let currentList;
let index = 0;
const metaData = DataModelHelper.getMetaData(node.options.version);
if (fieldValue instanceof Array) {
for (const activity of fieldValue) {
if (typeof activity === 'object' && activity !== null) {
found = false;
let activityIdentifier;
const inScheme = PropertyHelper.getObjectField(activity, 'inScheme', node.options.version);
const activityLists = [];
let listUrls = metaData.defaultActivityLists.slice();
if (typeof inScheme !== 'undefined') {
if (upgradeActivityLists.indexOf(inScheme) >= 0) {
errors.push(
this.createError(
'upgradeActivityList',
{
value: activity,
path: node.getPath(field, index),
},
{
list: inScheme,
},
),
);
} else if (metaData.defaultActivityLists.indexOf(inScheme) < 0) {
listUrls = [inScheme];
errors.push(
this.createError(
'useOfficialActivityList',
{
value: activity,
path: node.getPath(field, index),
},
),
);
}
} else {
errors.push(
this.createError(
'useOfficialActivityList',
{
value: activity,
path: node.getPath(field, index),
},
),
);
}
for (const listUrl of listUrls) {
const jsonResponse = await JsonLoaderHelper.getFile(listUrl, node.options);
if (
jsonResponse.errorCode === JsonLoaderHelper.ERROR_NONE
&& typeof jsonResponse.data === 'object'
&& jsonResponse.data !== null
) {
activityLists.push(jsonResponse.data);
if (typeof activityLists[activityLists.length - 1]['@url'] === 'undefined') {
activityLists[activityLists.length - 1]['@url'] = listUrl;
}
} else if (
jsonResponse.statusCode !== 200
&& jsonResponse.statusCode !== null
) {
errors.push(
this.createError(
'listErrorCode',
{
value: activity,
path: node.getPath(field, index),
},
{
list: listUrl,
code: jsonResponse.statusCode,
},
),
);
} else {
errors.push(
this.createError(
'listInvalid',
{
value: activity,
path: node.getPath(field, index),
},
{
list: listUrl,
},
),
);
}
}
for (const activityList of activityLists) {
currentList = activityList['@url'];
if (typeof activityList.concept !== 'undefined') {
for (const concept of activityList.concept) {
const prefLabel = PropertyHelper.getObjectField(activity, 'prefLabel', node.options.version);
const id = PropertyHelper.getObjectField(activity, 'id', node.options.version);
if (typeof prefLabel !== 'undefined') {
activityIdentifier = prefLabel;
prefLabelMatch = false;
correctPrefLabel = concept.prefLabel;
if (concept.prefLabel.toLowerCase() === prefLabel.toLowerCase()) {
found = true;
prefLabelMatch = true;
}
} else {
prefLabelMatch = true;
}
if (typeof id !== 'undefined') {
if (typeof prefLabel === 'undefined' || !prefLabelMatch) {
activityIdentifier = id;
}
idMatch = false;
correctId = concept.id;
if (concept.id === id) {
found = true;
idMatch = true;
}
} else {
idMatch = true;
}
if (found) {
break;
}
}
}
}
let errorKey;
let messageValues;
if (!found) {
errorKey = 'default';
messageValues = {
activity: activityIdentifier,
};
} else if (!prefLabelMatch) {
errorKey = 'noPrefLabelMatch';
messageValues = {
activity: activityIdentifier,
correctPrefLabel,
list: currentList,
};
} else if (!idMatch) {
errorKey = 'noIdMatch';
messageValues = {
activity: activityIdentifier,
correctId,
list: currentList,
};
}
if (errorKey) {
errors.push(
this.createError(
errorKey,
{
value: activity,
path: node.getPath(field, index),
},
messageValues,
),
);
}
}
index += 1;
}
}
return errors;
}
};