@openactive/data-model-validator
Version:
A library to allow a developer to validate a JSON document against the OpenActive Modelling Opportunity Specification
302 lines (287 loc) • 10.4 kB
JavaScript
const PropertyHelper = require('./property');
const GraphHelper = class {
static getClassGraph(spec, classId, version, subClassSpecs) {
if (typeof this.cache === 'undefined') {
this.cache = {};
}
if (typeof spec['@id'] !== 'undefined') {
if (typeof this.cache[spec['@id']] === 'undefined') {
this.cache[spec['@id']] = {};
}
if (typeof this.cache[spec['@id']][classId] !== 'undefined') {
return this.cache[spec['@id']][classId];
}
}
let classes = [];
const graphSpecs = [spec].concat(subClassSpecs);
for (const graphSpec of graphSpecs) {
if (typeof graphSpec['@graph'] !== 'undefined') {
const context = this.mergeContexts(graphSpec['@context']);
const graph = this.processGraph(graphSpec['@graph'], context, version);
if (graph instanceof Array) {
for (const item of graph) {
classes = this.processClass(graphSpec, item, classId, version, subClassSpecs);
if (classes.length > 0) {
break;
}
}
}
} else {
for (const itemKey in graphSpec) {
if (Object.prototype.hasOwnProperty.call(graphSpec, itemKey)) {
if (!itemKey.match(/^@/)) {
const item = graphSpec[itemKey];
classes = this.processClass(graphSpec, item, classId, version, subClassSpecs);
if (classes.length > 0) {
break;
}
}
}
}
}
if (classes.length > 0) {
break;
}
}
return classes;
}
static processClass(spec, item, classId, version, subClassSpecs) {
let classes = [];
const type = this.getProperty(spec, item, '@type', version);
if (typeof type === 'string') {
const id = this.getProperty(spec, item, '@id', version);
const context = this.mergeContexts(spec['@context']);
const classIdProp = PropertyHelper.getFullyQualifiedProperty(classId, version, { '@context': context });
const idProp = PropertyHelper.getFullyQualifiedProperty(id, version, { '@context': context });
const typeProp = PropertyHelper.getFullyQualifiedProperty(type, version, { '@context': context });
if (
typeProp.namespace === 'http://www.w3.org/2000/01/rdf-schema#'
&& typeProp.label === 'Class'
&& idProp.namespace !== null
&& idProp.label !== null
&& idProp.namespace === classIdProp.namespace
&& idProp.label === classIdProp.label
) {
classes.push(`${classIdProp.namespace}${classIdProp.label}`);
const subClassOf = this.getProperty(spec, item, 'http://www.w3.org/2000/01/rdf-schema#subClassOf', version);
if (
typeof subClassOf === 'object'
&& subClassOf !== null
) {
const subClassId = this.getProperty(spec, subClassOf, '@id', version);
if (typeof subClassId === 'string') {
classes = classes.concat(this.getClassGraph(spec, subClassId, version, subClassSpecs));
}
} else if (typeof subClassOf === 'string') {
classes = classes.concat(this.getClassGraph(spec, subClassOf, version, subClassSpecs));
}
if (typeof spec['@id'] !== 'undefined') {
this.cache[spec['@id']][classId] = classes;
}
}
}
return classes;
}
static isPropertyInClass(spec, typeName, classId, version, subClassSpecs = []) {
const classes = this.getClassGraph(spec, classId, version, subClassSpecs);
if (typeof spec['@graph'] !== 'undefined') {
const context = this.mergeContexts(spec['@context']);
const graph = this.processGraph(spec['@graph'], context, version);
if (graph instanceof Array) {
for (const item of graph) {
if (this.isProperty(spec, item, typeName, version)) {
return this.processProperty(spec, item, classes, version);
}
}
}
} else {
for (const itemKey in spec) {
if (Object.prototype.hasOwnProperty.call(spec, itemKey)) {
if (!itemKey.match(/^@/)) {
const item = spec[itemKey];
if (this.isProperty(spec, item, typeName, version)) {
return this.processProperty(spec, item, classes, version);
}
}
}
}
}
return this.isPropertyInClassReturn(GraphHelper.PROPERTY_NOT_FOUND);
}
static isPropertyInClassReturn(code, data = null) {
return {
code,
data,
};
}
static isProperty(spec, item, typeName, version) {
const type = this.getProperty(spec, item, '@type', version);
const id = this.getProperty(spec, item, '@id', version);
if (
(typeof type === 'undefined' || !this.isPropertyEqual(spec, type, 'rdfs:Class', version))
&& this.isPropertyEqual(spec, id, typeName, version)
) {
return true;
}
return false;
}
static processProperty(spec, item, classes, version) {
const supersededBy = this.getProperty(spec, item, 'schema:supersededBy', version);
let includes = this.getProperty(spec, item, 'schema:domainIncludes', version);
if (typeof includes === 'undefined') {
includes = this.getProperty(spec, item, 'rdfs:domain', version);
}
if (typeof includes !== 'undefined') {
if (classes.length === 0) {
return this.isPropertyInClassReturn(GraphHelper.PROPERTY_DOMAIN_NOT_FOUND);
}
if (!(includes instanceof Array)) {
includes = [includes];
}
const returnIncludes = [];
const context = this.mergeContexts(spec['@context']);
for (const include of includes) {
let includeId;
if (
typeof include === 'object'
&& include !== null
) {
includeId = this.getProperty(spec, include, '@id', version);
} else if (typeof include === 'string') {
includeId = include;
}
if (typeof includeId === 'string') {
const prop = PropertyHelper.getFullyQualifiedProperty(includeId, version, { '@context': context });
if (prop.namespace) {
returnIncludes.push(`${prop.namespace}${prop.label}`);
} else if (prop.prefix) {
returnIncludes.push(`${prop.prefix}:${prop.label}`);
} else {
returnIncludes.push(includeId);
}
for (const classItem of classes) {
if (this.isPropertyEqual(spec, includeId, classItem, version)) {
return this.isPropertyInClassReturn(GraphHelper.PROPERTY_FOUND, { supersededBy });
}
}
}
}
if (returnIncludes.length > 0) {
return this.isPropertyInClassReturn(GraphHelper.PROPERTY_NOT_IN_DOMAIN, returnIncludes);
}
}
return this.isPropertyInClassReturn(GraphHelper.PROPERTY_FOUND, { supersededBy });
}
static processGraph(graph, context, version) {
if (graph instanceof Array) {
return graph;
}
let combinedGraph = [];
if (
typeof graph === 'object'
&& graph !== null
&& typeof graph['@context'] === 'object'
) {
for (const prop in graph['@context']) {
if (Object.prototype.hasOwnProperty.call(graph['@context'], prop)) {
if (
typeof graph['@context'][prop] === 'object'
&& typeof graph['@context'][prop]['@reverse'] !== 'undefined'
) {
const qualifiedProp = PropertyHelper.getFullyQualifiedProperty(
graph['@context'][prop]['@reverse'],
version,
{ '@context': context },
);
if (
qualifiedProp.namespace === 'http://www.w3.org/2000/01/rdf-schema#'
&& qualifiedProp.label === 'isDefinedBy'
&& typeof graph[prop] === 'object'
&& graph[prop] instanceof Array
) {
combinedGraph = combinedGraph.concat(graph[prop]);
}
}
}
}
}
return combinedGraph;
}
static mergeContexts(contexts = []) {
// Merge the contexts into a flat structure
let mergedContext = {};
if (contexts instanceof Array) {
for (const context of contexts) {
// If this is not an object, we can't process it
if (typeof context === 'object' && context !== null) {
mergedContext = Object.assign(mergedContext, context);
}
}
} else if (typeof contexts === 'object' && contexts !== null) {
mergedContext = Object.assign(mergedContext, contexts);
}
return mergedContext;
}
static getProperty(spec, item, field, version) {
if (typeof item[field] !== 'undefined') {
return item[field];
}
if (typeof spec['@context'] === 'undefined') {
return undefined;
}
const context = this.mergeContexts(spec['@context']);
const prop = PropertyHelper.getFullyQualifiedProperty(field, version, { '@context': context });
if (
typeof prop.alias !== 'undefined'
&& prop.alias !== null
&& typeof item[prop.alias] !== 'undefined'
) {
return item[prop.alias];
}
if (
prop.prefix !== null
&& typeof item[`${prop.prefix}:${prop.label}`] !== 'undefined'
) {
return item[`${prop.prefix}:${prop.label}`];
}
if (
prop.namespace !== null
&& typeof item[`${prop.namespace}${prop.label}`] !== 'undefined'
) {
return item[`${prop.namespace}${prop.label}`];
}
return undefined;
}
static isPropertyEqual(spec, userSupplied, field, version) {
if (typeof spec['@context'] === 'undefined') {
return (field === userSupplied);
}
const context = this.mergeContexts(spec['@context']);
const prop = PropertyHelper.getFullyQualifiedProperty(field, version, { '@context': context });
if (
typeof prop.alias !== 'undefined'
&& prop.alias !== null
&& prop.alias === userSupplied
) {
return true;
}
if (
prop.prefix !== null
&& `${prop.prefix}:${prop.label}` === userSupplied
) {
return true;
}
if (
prop.namespace !== null
&& `${prop.namespace}${prop.label}` === userSupplied
) {
return true;
}
return false;
}
};
GraphHelper.PROPERTY_FOUND = 1;
GraphHelper.PROPERTY_NOT_FOUND = 2;
GraphHelper.PROPERTY_NOT_IN_DOMAIN = 3;
GraphHelper.PROPERTY_DOMAIN_NOT_FOUND = 4;
module.exports = GraphHelper;