rdf-validate-shacl
Version:
RDF SHACL validator
495 lines (494 loc) • 20.3 kB
JavaScript
import { validateTerm } from 'rdf-validate-datatype';
import { fromRdf } from 'rdf-literal';
import NodeSet from './node-set.js';
import { getPathObjects } from './property-path.js';
import { isInstanceOf, rdfListToArray } from './dataset-utils.js';
import ns from './namespaces.js';
const validateAnd = {
validate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const andNode = constraint.getParameterValue(sh.and);
const shapes = rdfListToArray(context.$shapes.node(andNode));
return shapes.every((shape) => {
if (constraint.shape.isPropertyShape) {
return context.nodeConformsToShape(focusNode, shape, constraint.pathObject);
}
return context.nodeConformsToShape(valueNode, shape);
});
},
};
const validateClass = {
validate(context, focusNode, valueNode, constraint) {
const classNode = constraint.getParameterValue(ns.sh.class);
return isInstanceOf(context.$data.node(valueNode), context.$data.node(classNode), context.ns);
},
};
const validateClosed = {
validate(context, focusNode, valueNode, constraint) {
const { sh, xsd } = context.ns;
const closedNode = constraint.getParameterValue(sh.closed);
const ignoredPropertiesNode = constraint.getParameterValue(sh.ignoredProperties);
const currentShape = constraint.shape.shapeNode;
const trueTerm = context.factory.literal('true', xsd.boolean);
if (!trueTerm.equals(closedNode)) {
return;
}
const allowed = new NodeSet(context.$shapes
.node(currentShape)
.out(sh.property)
.out(sh.path)
.terms
.filter((term) => term.termType === 'NamedNode'));
if (ignoredPropertiesNode) {
allowed.addAll(rdfListToArray(context.$shapes.node(ignoredPropertiesNode)));
}
const results = [];
const valueQuads = [...context.$data.dataset.match(valueNode, null, null)];
valueQuads
.filter(({ predicate }) => !allowed.has(predicate))
.forEach(({ predicate, object }) => {
results.push({ path: predicate, value: object });
});
return results;
},
validationMessage: 'Predicate is not allowed (closed shape)',
};
const validateDatatype = {
validate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const datatypeNode = constraint.getParameterValue(sh.datatype);
if (valueNode.termType === 'Literal') {
return valueNode.datatype.equals(datatypeNode) && validateTerm(valueNode);
}
else {
return false;
}
},
validationMessage: 'Value does not have datatype {$datatype}',
};
const validateDisjoint = {
validate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const disjointNode = constraint.getParameterValue(sh.disjoint);
return context.$data.dataset.match(focusNode, disjointNode, valueNode).size === 0;
},
validationMessage: 'Value node must not also be one of the values of {$disjoint}',
};
const validateEquals = {
propertyValidate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const path = constraint.shape.pathObject;
const equalsNode = constraint.getParameterValue(sh.equals);
const results = [];
getPathObjects(context.$data, focusNode, path).forEach(value => {
if (context.$data.dataset.match(focusNode, equalsNode, value).size === 0) {
results.push({ value });
}
});
const equalsQuads = [...context.$data.dataset.match(focusNode, equalsNode, null)];
equalsQuads.forEach(({ object }) => {
const value = object;
if (!getPathObjects(context.$data, focusNode, path).some(pathValue => pathValue.equals(value))) {
results.push({ value });
}
});
return results;
},
propertyValidationMessage: 'Must have same values as {$equals}',
nodeValidate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const equalsNode = constraint.getParameterValue(sh.equals);
const results = [];
let solutions = 0;
getPathObjects(context.$data, focusNode, equalsNode).forEach(value => {
solutions++;
if (!value.equals(focusNode)) {
results.push({ value });
}
});
if (results.length === 0 && solutions === 0) {
results.push({ value: focusNode });
}
return results;
},
nodeValidationMessage: 'Must have same values as {$equals}',
};
const validateHasValue = {
nodeValidate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const hasValueNode = constraint.getParameterValue(sh.hasValue);
return focusNode.equals(hasValueNode);
},
nodeValidationMessage: 'Value must be {$hasValue}',
propertyValidate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const path = constraint.shape.pathObject;
const hasValueNode = constraint.getParameterValue(sh.hasValue);
return getPathObjects(context.$data, focusNode, path)
.some(value => value.equals(hasValueNode));
},
propertyValidationMessage: 'Missing expected value {$hasValue}',
};
const validateIn = {
validate(context, focusNode, valueNode, constraint) {
return constraint.nodeSet.has(valueNode);
},
validationMessage: 'Value is not one of the allowed values: {$in}',
};
const validateLanguageIn = {
validate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
if (valueNode.termType !== 'Literal') {
return false;
}
const valueLanguage = valueNode.language;
if (!valueLanguage || valueLanguage === '') {
return false;
}
const languageInNode = constraint.getParameterValue(sh.languageIn);
const allowedLanguages = rdfListToArray(context.$shapes.node(languageInNode));
return allowedLanguages.some(allowedLanguage => valueLanguage.startsWith(allowedLanguage.value));
},
validationMessage: 'Language does not match any of {$languageIn}',
};
const validateLessThan = {
propertyValidate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const valuePath = constraint.shape.pathObject;
const values = getPathObjects(context.$data, focusNode, valuePath);
const lessThanNode = constraint.getParameterValue(sh.lessThan);
const referenceValues = context.$data.node(focusNode).out(lessThanNode).terms;
const invalidValues = [];
for (const value of values) {
for (const referenceValue of referenceValues) {
const c = compareTerms(value, referenceValue, context.ns);
if (c === null || c >= 0) {
invalidValues.push({ value });
}
}
}
return invalidValues;
},
propertyValidationMessage: 'Value is not less than value of {$lessThan}',
};
const validateLessThanOrEquals = {
propertyValidate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const valuePath = constraint.shape.pathObject;
const values = getPathObjects(context.$data, focusNode, valuePath);
const lessThanOrEqualsNode = constraint.getParameterValue(sh.lessThanOrEquals);
const referenceValues = context.$data.node(focusNode).out(lessThanOrEqualsNode).terms;
const invalidValues = [];
for (const value of values) {
for (const referenceValue of referenceValues) {
const c = compareTerms(value, referenceValue, context.ns);
if (c === null || c > 0) {
invalidValues.push({ value });
}
}
}
return invalidValues;
},
propertyValidationMessage: 'Value is not less than or equal to value of {$lessThanOrEquals}',
};
const validateMaxCount = {
propertyValidate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const path = constraint.shape.pathObject;
const count = getPathObjects(context.$data, focusNode, path).length;
const maxCountNode = constraint.getParameterValue(sh.maxCount);
return maxCountNode && count <= Number(maxCountNode.value);
},
propertyValidationMessage: 'More than {$maxCount} values',
};
const validateMaxExclusive = {
validate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const maxExclusiveNode = constraint.getParameterValue(sh.maxExclusive);
const comp = compareTerms(valueNode, maxExclusiveNode, context.ns);
return (comp !== null && comp < 0);
},
validationMessage: 'Value is not less than {$maxExclusive}',
};
const validateMaxInclusive = {
validate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const maxInclusiveNode = constraint.getParameterValue(sh.maxInclusive);
const comp = compareTerms(valueNode, maxInclusiveNode, context.ns);
return (comp !== null && comp <= 0);
},
validationMessage: 'Value is not less than or equal to {$maxInclusive}',
};
const validateMaxLength = {
validate(context, focusNode, valueNode, constraint) {
if (valueNode.termType === 'BlankNode') {
return false;
}
const { sh } = context.ns;
const maxLengthNode = constraint.getParameterValue(sh.maxLength);
return valueNode.value.length <= Number(maxLengthNode.value);
},
validationMessage: 'Value has more than {$maxLength} characters',
};
const validateMinCount = {
propertyValidate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const path = constraint.pathObject;
const count = getPathObjects(context.$data, focusNode, path).length;
const minCountNode = constraint.getParameterValue(sh.minCount);
return count >= Number(minCountNode.value);
},
propertyValidationMessage: 'Less than {$minCount} values',
};
const validateMinExclusive = {
validate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const minExclusiveNode = constraint.getParameterValue(sh.minExclusive);
const comp = compareTerms(valueNode, minExclusiveNode, context.ns);
return (comp !== null && comp > 0);
},
validationMessage: 'Value is not greater than {$minExclusive}',
};
const validateMinInclusive = {
validate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const minInclusiveNode = constraint.getParameterValue(sh.minInclusive);
const comp = compareTerms(valueNode, minInclusiveNode, context.ns);
return (comp !== null && comp >= 0);
},
validationMessage: 'Value is not greater than or equal to {$minInclusive}',
};
const validateMinLength = {
validate(context, focusNode, valueNode, constraint) {
if (valueNode.termType === 'BlankNode') {
return false;
}
const { sh } = context.ns;
const minLengthNode = constraint.getParameterValue(sh.minLength);
return valueNode.value.length >= Number(minLengthNode.value);
},
validationMessage: 'Value has less than {$minLength} characters',
};
const validateNodeKind = {
validate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const nodeKindNode = constraint.getParameterValue(sh.nodeKind);
if (valueNode.termType === 'BlankNode') {
return sh.BlankNode.equals(nodeKindNode) ||
sh.BlankNodeOrIRI.equals(nodeKindNode) ||
sh.BlankNodeOrLiteral.equals(nodeKindNode);
}
else if (valueNode.termType === 'NamedNode') {
return sh.IRI.equals(nodeKindNode) ||
sh.BlankNodeOrIRI.equals(nodeKindNode) ||
sh.IRIOrLiteral.equals(nodeKindNode);
}
else if (valueNode.termType === 'Literal') {
return sh.Literal.equals(nodeKindNode) ||
sh.BlankNodeOrLiteral.equals(nodeKindNode) ||
sh.IRIOrLiteral.equals(nodeKindNode);
}
},
validationMessage: 'Value does not have node kind {$nodeKind}',
};
const validateNode = {
validate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const nodeNode = constraint.getParameterValue(sh.node);
return context.validateNodeAgainstShape(valueNode, nodeNode);
},
validationMessage: 'Value does not have shape {$node}',
};
const validateNot = {
validate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const notNode = constraint.getParameterValue(sh.not);
return !context.nodeConformsToShape(valueNode, notNode);
},
validationMessage: 'Value does have shape {$not}',
};
const validateOr = {
validate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const orNode = constraint.getParameterValue(sh.or);
const shapes = rdfListToArray(context.$shapes.node(orNode));
return shapes.some(shape => context.nodeConformsToShape(valueNode, shape));
},
};
const validatePattern = {
validate(context, focusNode, valueNode, constraint) {
if (valueNode.termType === 'BlankNode') {
return false;
}
const { sh } = context.ns;
const flagsNode = constraint.getParameterValue(sh.flags);
const patternNode = constraint.getParameterValue(sh.pattern);
const re = flagsNode ? new RegExp(patternNode.value, flagsNode.value) : new RegExp(patternNode.value);
return re.test(valueNode.value);
},
validationMessage: 'Value does not match pattern "{$pattern}"',
};
const validateQualifiedMaxCount = {
propertyValidate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const count = validateQualifiedHelper(context, focusNode, constraint);
const qualifiedMaxCountNode = constraint.getParameterValue(sh.qualifiedMaxCount);
return qualifiedMaxCountNode.termType === 'Literal' && count <= Number(qualifiedMaxCountNode.value);
},
propertyValidationMessage: 'More than {$qualifiedMaxCount} values have shape {$qualifiedValueShape}',
};
const validateQualifiedMinCount = {
propertyValidate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const count = validateQualifiedHelper(context, focusNode, constraint);
const qualifiedMinCountNode = constraint.getParameterValue(sh.qualifiedMinCount);
return qualifiedMinCountNode.termType === 'Literal' && count >= Number(qualifiedMinCountNode.value);
},
propertyValidationMessage: 'Less than {$qualifiedMinCount} values have shape {$qualifiedValueShape}',
};
function validateQualifiedHelper(context, focusNode, constraint) {
const { sh, xsd } = context.ns;
const currentShapeNode = constraint.shape.shapeNode;
const qualifiedValueShapesDisjointNode = constraint.getParameterValue(sh.qualifiedValueShapesDisjoint);
const qualifiedValueShapeNode = constraint.getParameterValue(sh.qualifiedValueShape);
const trueTerm = context.factory.literal('true', xsd.boolean);
const siblingShapes = new NodeSet();
if (trueTerm.equals(qualifiedValueShapesDisjointNode)) {
const qualifiedSiblingShapes = context.$shapes
.node(currentShapeNode)
// Move up to parent
.in(sh.property)
// Move down to all siblings
.out(sh.property)
// Select sh:qualifiedValueShape of all siblings
.out(sh.qualifiedValueShape)
.filter(({ term }) => !term.equals(qualifiedValueShapeNode))
.terms;
siblingShapes.addAll(qualifiedSiblingShapes);
}
const path = constraint.shape.pathObject;
return getPathObjects(context.$data, focusNode, path)
.filter(value => context.nodeConformsToShape(value, qualifiedValueShapeNode) &&
!validateQualifiedConformsToASibling(context, value, [...siblingShapes]))
.length;
}
function validateQualifiedConformsToASibling(context, value, siblingShapes) {
for (let i = 0; i < siblingShapes.length; i++) {
if (context.nodeConformsToShape(value, siblingShapes[i])) {
return true;
}
}
return false;
}
const validateUniqueLang = {
propertyValidate(context, focusNode, valueNode, constraint) {
const { sh, xsd } = context.ns;
const uniqueLangNode = constraint.getParameterValue(sh.uniqueLang);
const trueTerm = context.factory.literal('true', xsd.boolean);
if (!trueTerm.equals(uniqueLangNode)) {
return;
}
const path = constraint.shape.pathObject;
const map = {};
getPathObjects(context.$data, focusNode, path).forEach(value => {
if (value.termType === 'Literal' && value.language && value.language !== '') {
const old = map[value.language];
if (!old) {
map[value.language] = 1;
}
else {
map[value.language] = old + 1;
}
}
});
const results = [];
for (const lang in map) {
if (Object.prototype.hasOwnProperty.call(map, lang)) {
const count = map[lang];
if (count > 1) {
results.push('Language "' + lang + '" has been used by ' + count + ' values');
}
}
}
return results;
},
propertyValidationMessage: 'Language "{?lang}" used more than once',
};
const validateXone = {
validate(context, focusNode, valueNode, constraint) {
const { sh } = context.ns;
const xoneNode = constraint.getParameterValue(sh.xone);
const shapes = rdfListToArray(context.$shapes.node(xoneNode));
const conformsCount = shapes
.map(shape => context.nodeConformsToShape(valueNode, shape))
.filter(Boolean)
.length;
return conformsCount === 1;
},
};
// Private helper functions
/**
* Compare 2 terms.
*
* Returns:
* - a negative number if term1 occurs before term2
* - a positive number if the term1 occurs after term2
* - 0 if they are equivalent
* - null if they are not comparable
*/
function compareTerms(term1, term2, ns) {
if (!term1 || !term2 || term1.termType !== 'Literal' || term2.termType !== 'Literal') {
return null;
}
// Check that if one of the compared nodes is a datetime with a timezone,
// the other one is too. A datetime with a specified timezone is not comparable
// with a datetime without a timezone.
if (hasTimezone(term1, ns) !== hasTimezone(term2, ns)) {
return null;
}
const value1 = fromRdf(term1);
const value2 = fromRdf(term2);
if (typeof value1 !== typeof value2) {
return null;
}
if (typeof value1 === 'string') {
return value1.localeCompare(value2);
}
else {
return value1 - value2;
}
}
function hasTimezone(node, ns) {
const pattern = /^.*(((\+|-)\d{2}:\d{2})|Z)$/;
return ns.xsd.dateTime.equals(node.datatype) && pattern.test(node.value);
}
export default {
validateAnd,
validateClass,
validateClosed,
validateDatatype,
validateDisjoint,
validateEquals,
validateHasValue,
validateIn,
validateLanguageIn,
validateLessThan,
validateLessThanOrEquals,
validateMaxCount,
validateMaxExclusive,
validateMaxInclusive,
validateMaxLength,
validateMinCount,
validateMinExclusive,
validateMinInclusive,
validateMinLength,
validateNode,
validateNodeKind,
validateNot,
validateOr,
validatePattern,
validateQualifiedMaxCount,
validateQualifiedMinCount,
validateUniqueLang,
validateXone,
};