UNPKG

rdf-validate-shacl

Version:
306 lines (305 loc) 12.8 kB
/* eslint-disable no-use-before-define, camelcase */ // Design: // // First, derive a ShapesGraph object from the definitions in $shapes. // This manages a map of parameters to ConstraintComponents. // Each ConstraintComponent manages its list of parameters and a link to the validators. // // The ShapesGraph also manages a list of Shapes, each which has a list of Constraints. // A Constraint is a specific combination of parameters for a constraint component, // and has functions to access the target nodes. // // Each ShapesGraph can be reused between validation calls, and thus often only needs // to be created once per application. // // The validation process is started by creating a ValidationEngine that relies on // a given ShapesGraph and operates on the current $data(). // It basically walks through all Shapes that have target nodes and runs the validators // for each Constraint of the shape, producing results along the way. import shaclVocabularyFactory from '@vocabulary/sh'; import NodeSet from './node-set.js'; import { extractPropertyPath, getPathObjects } from './property-path.js'; import { getInstancesOf, isInstanceOf, rdfListToArray } from './dataset-utils.js'; class ShapesGraph { _components; _parametersMap; _shapes; _shapeNodesWithConstraints; _shapesWithTarget; constructor(context) { this.context = context; // Collect all defined constraint components const { sh } = context.ns; const shaclVocabulary = context.factory.clownface({ dataset: context.factory.dataset(shaclVocabularyFactory(context)), }); const componentNodes = getInstancesOf(shaclVocabulary.node(sh.ConstraintComponent), context.ns); this._components = [...componentNodes].map((node) => new ConstraintComponent(node, context, shaclVocabulary)); // Build map from parameters to constraint components this._parametersMap = new Map(); for (const component of this._components) { for (const parameter of component.parameters) { this._parametersMap.set(parameter.value, component); } } // Cache of shapes populated on demand this._shapes = new Map(); } getComponentWithParameter(parameter) { return this._parametersMap.get(parameter.value); } getShape(shapeNode) { if (!this._shapes.has(shapeNode.value)) { const shape = new Shape(this.context, shapeNode); this._shapes.set(shapeNode.value, shape); } return this._shapes.get(shapeNode.value); } get shapeNodesWithConstraints() { if (!this._shapeNodesWithConstraints) { const set = new NodeSet(); for (const component of this._components) { const params = component.requiredParameters; for (const param of params) { const shapesWithParam = [...this.context.$shapes.dataset .match(null, param, null)] .map(({ subject }) => subject); set.addAll(shapesWithParam); } } this._shapeNodesWithConstraints = [...set]; } return this._shapeNodesWithConstraints; } get shapesWithTarget() { const { $shapes, ns } = this.context; const { rdfs, sh } = ns; if (!this._shapesWithTarget) { this._shapesWithTarget = this.shapeNodesWithConstraints .filter((shapeNode) => (isInstanceOf($shapes.node(shapeNode), $shapes.node(rdfs.Class), ns) || $shapes.node(shapeNode).out([ sh.targetClass, sh.targetNode, sh.targetSubjectsOf, sh.targetObjectsOf, sh.target, ]).terms.length > 0)) .map((shapeNode) => this.getShape(shapeNode)); } return this._shapesWithTarget; } } export class Constraint { shape; component; paramValue; _parameterValues; inNodeSet; // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(shape, component, shapesGraph, _parameterValuesOrSingleParam) { this.shape = shape; this.component = component; this.shapeNodePointer = shapesGraph.node(shape.shapeNode); if ('termType' in _parameterValuesOrSingleParam) { this.paramValue = _parameterValuesOrSingleParam; } else { this._parameterValues = _parameterValuesOrSingleParam; } } get validate() { if (this.component.validator && this.validationFunction) { return (focusNode, valueNode) => { return this.validationFunction(focusNode, valueNode, this); }; } } static *fromShape(shape, component, shapesGraph) { const allParams = component.parameters.map((param) => { return [param, shape.shapeNodePointer.out(param).terms]; }); // create a cartesian product of all parameter values const combinations = allParams.reduce((acc, [param, values]) => { if (values.length === 0) { return acc; } if (acc.length === 0) { return values.map((value) => [[param, value]]); } return acc.flatMap((comb) => values.map((value) => comb.concat([[param, value]]))); }, []); for (const combination of combinations) { if (component.parameters.length === 1) { yield new Constraint(shape, component, shapesGraph, combination[0][1]); continue; } const params = shape.context.factory.termMap(combination); if (component.isComplete(params)) { yield new Constraint(shape, component, shapesGraph, params); } } } getParameterValue(param) { return this.paramValue || this._parameterValues.get(param); } get pathObject() { return this.shape.pathObject; } get validationFunction() { return this.shape.isPropertyShape ? this.component.propertyValidationFunction : this.component.nodeValidationFunction; } get isValidationFunctionGeneric() { return this.shape.isPropertyShape ? this.component.propertyValidationFunctionGeneric : this.component.nodeValidationFunctionGeneric; } get componentMessages() { return this.component.getMessages(this.shape); } get nodeSet() { const { sh } = this.shape.context.ns; if (!this.inNodeSet) { this.inNodeSet = new NodeSet(rdfListToArray(this.shapeNodePointer.out(sh.in))); } return this.inNodeSet; } } class ConstraintComponent { node; context; constructor(node, context, shaclVocabulary) { this.node = node; this.context = context; const { factory, ns } = context; const { sh, xsd } = ns; this.nodePointer = shaclVocabulary.node(node); this.parameters = []; this.parameterNodes = []; this.requiredParameters = []; this.optionals = {}; const trueTerm = factory.literal('true', xsd.boolean); this.nodePointer .out(sh.parameter) .forEach((parameterCf) => { const parameter = parameterCf.term; parameterCf.out(sh.path).forEach(({ term: path }) => { this.parameters.push(path); this.parameterNodes.push(parameter); if (shaclVocabulary.dataset.match(parameter, sh.optional, trueTerm).size > 0) { this.optionals[path.value] = true; } else { this.requiredParameters.push(path); } }); }); this.validator = context.validators.get(node); if (!this.validator) { return; } if ('nodeValidate' in this.validator) { this.nodeValidationFunction = this.validator.nodeValidate.bind(undefined, this.context); this.nodeValidationMessage = this.validator.nodeValidationMessage; } else if ('validate' in this.validator) { this.nodeValidationFunction = this.validator.validate.bind(undefined, this.context); this.nodeValidationMessage = this.validator.validationMessage; this.nodeValidationFunctionGeneric = true; } if ('propertyValidate' in this.validator) { this.propertyValidationFunction = this.validator.propertyValidate.bind(undefined, this.context); this.propertyValidationMessage = this.validator.propertyValidationMessage; } else if ('validate' in this.validator) { this.propertyValidationFunction = this.validator.validate.bind(undefined, this.context); this.propertyValidationMessage = this.validator.validationMessage; this.propertyValidationFunctionGeneric = true; } } getMessages(shape) { const message = shape.isPropertyShape ? this.propertyValidationMessage : this.nodeValidationMessage; return message ? [message] : []; } isComplete(parameterValues) { return this.requiredParameters.every((param) => parameterValues.has(param)); } } export class Shape { constructor(context, shapeNode) { const { $shapes, ns, shapesGraph, allowNamedNodeInList: allowNamedNodeSequencePaths } = context; const { sh } = ns; this.context = context; this.shapeNode = shapeNode; this.shapeNodePointer = $shapes.node(shapeNode); this.severity = this.shapeNodePointer.out(sh.severity).term || sh.Violation; this.deactivated = this.shapeNodePointer.out(sh.deactivated).value === 'true'; this.pathObject = null; const path = this.shapeNodePointer.out(sh.path); if (path.term) { this.path = path; this.pathObject = extractPropertyPath(this.path, ns, allowNamedNodeSequencePaths); } this.constraints = []; const handled = new NodeSet(); const shapeProperties = [...$shapes.dataset.match(shapeNode, null, null)]; shapeProperties.forEach((sol) => { const component = shapesGraph.getComponentWithParameter(sol.predicate); if (component && !handled.has(component.node)) { this.constraints.push(...Constraint.fromShape(this, component, $shapes)); handled.add(component.node); } }); } get isPropertyShape() { return this.pathObject != null; } overridePath(path) { const shape = new Shape(this.context, this.shapeNode); shape.pathObject = path; return shape; } getTargetNodes(dataGraph) { const { $shapes, ns } = this.context; const { rdfs, sh } = ns; const results = new NodeSet(); if (isInstanceOf($shapes.node(this.shapeNode), $shapes.node(rdfs.Class), ns)) { results.addAll(getInstancesOf(dataGraph.node(this.shapeNode), ns)); } const targetClasses = [...$shapes.dataset.match(this.shapeNode, sh.targetClass, null)]; targetClasses.forEach(({ object: targetClass }) => { results.addAll(getInstancesOf(dataGraph.node(targetClass), ns)); }); const targetNodes = this.shapeNodePointer.out(sh.targetNode).terms // Ensure the node exists in data graph before considering it as a validatable target node .filter((targetNode) => (dataGraph.dataset.match(targetNode).size > 0 || dataGraph.dataset.match(null, targetNode).size > 0 || dataGraph.dataset.match(null, null, targetNode).size > 0)); results.addAll(targetNodes); this.shapeNodePointer .out(sh.targetSubjectsOf) .terms .forEach((predicate) => { const subjects = [...dataGraph.dataset.match(null, predicate, null)].map(({ subject }) => subject); results.addAll(subjects); }); this.shapeNodePointer .out(sh.targetObjectsOf) .terms .forEach((predicate) => { const objects = [...dataGraph.dataset.match(null, predicate, null)].map(({ object }) => object); results.addAll(objects); }); return [...results]; } getValueNodes(focusNode, dataGraph) { if (this.pathObject) { return getPathObjects(dataGraph, focusNode, this.pathObject); } else { return [focusNode]; } } } export default ShapesGraph;