rdf-validate-shacl
Advanced tools
Comparing version 0.1.1 to 0.1.2
@@ -10,2 +10,11 @@ | ||
## 0.1.2 (2020-04-21) | ||
* Include deep blank node structures in validation report | ||
* Add official SHACL test suite | ||
* Fix provided factory not being used to create all quads in the validation | ||
report | ||
* Performance improvements | ||
## 0.1.1 (2020-04-07) | ||
@@ -12,0 +21,0 @@ |
{ | ||
"name": "rdf-validate-shacl", | ||
"version": "0.1.1", | ||
"version": "0.1.2", | ||
"description": "RDF SHACL validator", | ||
@@ -23,7 +23,7 @@ "main": "index.js", | ||
"@rdfjs/term-set": "~1.0.1", | ||
"clownface": "^0.12.1", | ||
"clownface": "^0.12.3", | ||
"rdf-validate-datatype": "^0.1.1" | ||
}, | ||
"devDependencies": { | ||
"@rdfjs/parser-n3": "^1.1.3", | ||
"@rdfjs/parser-n3": "^1.1.4", | ||
"@zazuko/rdf-vocabularies": "^2020.3.23", | ||
@@ -33,5 +33,5 @@ "debug": "^4.1.1", | ||
"mocha": "^7.1.1", | ||
"nyc": "^15.0.0", | ||
"nyc": "^15.0.1", | ||
"rdf-ext": "^1.3.0", | ||
"rdf-utils-fs": "^2.1.0", | ||
"rdf-utils-dataset": "1.1.0", | ||
"standard": "^14.3.3" | ||
@@ -38,0 +38,0 @@ }, |
# rdf-validate-shacl | ||
RDF/JS SHACL validator | ||
JavaScript SHACL validation ([RDF/JS](https://rdf.js.org/) compatible) | ||
@@ -11,11 +11,29 @@ [](https://travis-ci.org/zazuko/rdf-validate-shacl) | ||
Create a new SHACL validator and load data and shapes to trigger the validation. | ||
The library only handles SHACL validation and not data loading/parsing. | ||
The following example uses [rdf-utils-fs](https://github.com/rdf-ext/rdf-utils-fs) | ||
for this purpose. For more information about handling RDF data in JavaScript, | ||
check out [Get started with RDF in JavaScript](https://zazuko.com/get-started/developers/). | ||
The validation function returns a `ValidationReport` object that can be used | ||
to inspect conformance and results. | ||
to inspect conformance and results. The `ValidationReport` also has a | ||
`.dataset` property, which provides the report as RDF data. | ||
```javascript | ||
const validator = new SHACLValidator(shapesDataset) | ||
const report = await validator.validate(dataDataset) | ||
const fs = require('fs') | ||
const factory = require('rdf-ext') | ||
const ParserN3 = require('@rdfjs/parser-n3') | ||
const SHACLValidator = require('rdf-validate-shacl') | ||
async function loadDataset (filePath) { | ||
const stream = fs.createReadStream(filePath) | ||
const parser = new ParserN3({ factory }) | ||
return factory.dataset().import(parser.import(stream)) | ||
} | ||
const shapes = await loadDataset('my-shapes.ttl') | ||
const data = await loadDataset('my-data.ttl') | ||
const validator = new SHACLValidator(shapes, { factory }) | ||
const report = await validator.validate(data) | ||
// Check conformance: `true` or `false` | ||
@@ -34,2 +52,5 @@ console.log(report.conforms) | ||
} | ||
// Validation report as RDF dataset | ||
console.log(report.dataset) | ||
``` | ||
@@ -36,0 +57,0 @@ |
@@ -1,5 +0,70 @@ | ||
const NodeSet = require('./node-set') | ||
const { rdf, sh } = require('./namespaces') | ||
/** | ||
* Extracts all the nodes of a property path from a graph and returns a | ||
* property path object. | ||
* | ||
* @param {RDFLibGraph} graph | ||
* @param {Term} pathNode - Start node of the path | ||
* @return Property path object | ||
*/ | ||
function extractPropertyPath (graph, pathNode) { | ||
if (pathNode.termType === 'NamedNode') { | ||
return pathNode | ||
} | ||
if (pathNode.termType === 'BlankNode') { | ||
const pathCf = graph.cf.node(pathNode) | ||
const first = pathCf.out(rdf.first).term | ||
if (first) { | ||
const paths = graph.rdfListToArray(pathNode) | ||
return paths.map(path => extractPropertyPath(graph, path)) | ||
} | ||
const alternativePath = pathCf.out(sh.alternativePath).term | ||
if (alternativePath) { | ||
const paths = graph.rdfListToArray(alternativePath) | ||
return { or: paths.map(path => extractPropertyPath(graph, path)) } | ||
} | ||
const zeroOrMorePath = pathCf.out(sh.zeroOrMorePath).term | ||
if (zeroOrMorePath) { | ||
return { zeroOrMore: extractPropertyPath(graph, zeroOrMorePath) } | ||
} | ||
const oneOrMorePath = pathCf.out(sh.oneOrMorePath).term | ||
if (oneOrMorePath) { | ||
return { oneOrMore: extractPropertyPath(graph, oneOrMorePath) } | ||
} | ||
const zeroOrOnePath = pathCf.out(sh.zeroOrOnePath).term | ||
if (zeroOrOnePath) { | ||
return { zeroOrOne: extractPropertyPath(graph, zeroOrOnePath) } | ||
} | ||
const inversePath = pathCf.out(sh.inversePath).term | ||
if (inversePath) { | ||
return { inverse: extractPropertyPath(graph, inversePath) } | ||
} | ||
} | ||
throw new Error(`Unsupported SHACL path: ${pathNode.value}`) | ||
} | ||
/** | ||
* Follows a property path in a graph, starting from a given node, and returns | ||
* all the nodes it points to. | ||
* | ||
* @param {RDFLibGraph} graph | ||
* @param {Term} subject - Start node | ||
* @param {object} path - Property path object | ||
* @return {Term[]} - Nodes that are reachable through the property path | ||
*/ | ||
function getPathObjects (graph, subject, path) { | ||
return [...getPathObjectsSet(graph, subject, path)] | ||
} | ||
function getPathObjectsSet (graph, subject, path) { | ||
if (path.termType === 'NamedNode') { | ||
@@ -33,3 +98,3 @@ return getNamedNodePathObjects(graph, subject, path) | ||
subjects = new NodeSet(flatMap(subjects, subjectItem => | ||
[...getPathObjects(graph, subjectItem, pathItem)])) | ||
getPathObjects(graph, subjectItem, pathItem))) | ||
} | ||
@@ -40,3 +105,3 @@ return subjects | ||
function getOrPathObjects (graph, subject, path) { | ||
return new NodeSet(flatMap(path.or, pathItem => [...getPathObjects(graph, subject, pathItem)])) | ||
return new NodeSet(flatMap(path.or, pathItem => getPathObjects(graph, subject, pathItem))) | ||
} | ||
@@ -53,3 +118,3 @@ | ||
function getZeroOrOnePathObjects (graph, subject, path) { | ||
const pathObjects = getPathObjects(graph, subject, path.zeroOrOne) | ||
const pathObjects = getPathObjectsSet(graph, subject, path.zeroOrOne) | ||
pathObjects.add(subject) | ||
@@ -74,3 +139,3 @@ return pathObjects | ||
const pathValues = getPathObjects(graph, subject, path) | ||
const pathValues = getPathObjectsSet(graph, subject, path) | ||
@@ -94,3 +159,4 @@ const deeperValues = flatMap(pathValues, pathValue => { | ||
module.exports = { | ||
extractPropertyPath, | ||
getPathObjects | ||
} |
const clownface = require('clownface') | ||
const isMatch = require('@rdfjs/dataset/isMatch') | ||
const { getPathObjects } = require('./property-path') | ||
const NodeSet = require('./node-set') | ||
@@ -17,18 +15,4 @@ const { rdf, rdfs } = require('./namespaces') | ||
hasMatch (s, p, o) { | ||
for (const quad of this.dataset) { | ||
if (isMatch(quad, s, p, o)) { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
getPathObjects (subject, path) { | ||
return [...getPathObjects(this, subject, path)] | ||
} | ||
get cf () { | ||
return clownface({ dataset: this.dataset }) | ||
return clownface({ dataset: this.dataset, factory: this.factory }) | ||
} | ||
@@ -73,11 +57,5 @@ | ||
rdfListToArray ($rdfList) { | ||
const items = [] | ||
while (!$rdfList.equals(rdf.nil)) { | ||
const first = this.cf.node($rdfList).out(rdf.first).term | ||
items.push(first) | ||
const rest = this.cf.node($rdfList).out(rdf.rest).term | ||
$rdfList = rest | ||
} | ||
return items | ||
rdfListToArray (listNode) { | ||
const iterator = this.cf.node(listNode).list() | ||
return [...iterator].map(({ term }) => term) | ||
} | ||
@@ -84,0 +62,0 @@ } |
@@ -22,3 +22,3 @@ // Design: | ||
const validatorsRegistry = require('./validators-registry') | ||
const { toRDFQueryPath } = require('./validators') | ||
const { extractPropertyPath, getPathObjects } = require('./property-path') | ||
const { rdfs, sh } = require('./namespaces') | ||
@@ -85,7 +85,9 @@ | ||
$shapes.isInstanceOf(shapeNode, rdfs.Class) || | ||
$shapes.hasMatch(shapeNode, sh.targetClass, null) || | ||
$shapes.hasMatch(shapeNode, sh.targetNode, null) || | ||
$shapes.hasMatch(shapeNode, sh.targetSubjectsOf, null) || | ||
$shapes.hasMatch(shapeNode, sh.targetObjectsOf, null) || | ||
$shapes.hasMatch(shapeNode, sh.target, null) | ||
$shapes.cf.node(shapeNode).out([ | ||
sh.targetClass, | ||
sh.targetNode, | ||
sh.targetSubjectsOf, | ||
sh.targetObjectsOf, | ||
sh.target | ||
]).terms.length > 0 | ||
) { | ||
@@ -137,3 +139,3 @@ this.targetShapes.push(this.getShape(shapeNode)) | ||
this.parameterNodes.push(parameter) | ||
if (this.context.$shapes.hasMatch(parameter, sh.optional, this.factory.true)) { | ||
if (this.context.$shapes.match(parameter, sh.optional, this.factory.true).size > 0) { | ||
this.optionals[path.value] = true | ||
@@ -192,3 +194,3 @@ } else { | ||
this.isRequired(parameter.value) && | ||
!this.context.$shapes.hasMatch(shapeNode, parameter, null) | ||
this.context.$shapes.match(shapeNode, parameter, null).size === 0 | ||
)) | ||
@@ -212,2 +214,3 @@ } | ||
this.path = context.$shapes.cf.node(shapeNode).out(sh.path).term | ||
this._pathObject = undefined | ||
this.shapeNode = shapeNode | ||
@@ -232,2 +235,13 @@ this.constraints = [] | ||
/** | ||
* Property path object | ||
*/ | ||
get pathObject () { | ||
if (this._pathObject === undefined) { | ||
this._pathObject = this.path ? extractPropertyPath(this.context.$shapes, this.path) : null | ||
} | ||
return this._pathObject | ||
} | ||
getTargetNodes (rdfDataGraph) { | ||
@@ -268,6 +282,5 @@ const results = new NodeSet() | ||
getValueNodes (focusNode, rdfDataGraph) { | ||
getValueNodes (focusNode, dataGraph) { | ||
if (this.path) { | ||
const path = toRDFQueryPath(this.context.$shapes, this.path) | ||
return rdfDataGraph.getPathObjects(focusNode, path) | ||
return getPathObjects(dataGraph, focusNode, this.pathObject) | ||
} else { | ||
@@ -274,0 +287,0 @@ return [focusNode] |
const ValidationReport = require('./validation-report') | ||
const { extractStructure } = require('./dataset-utils') | ||
const error = require('debug')('validation-enging::error') | ||
@@ -32,6 +33,6 @@ | ||
this.addResultProperty(result, sh.sourceConstraintComponent, sourceConstraintComponent) | ||
this.addResultProperty(result, sh.sourceShape, sourceShape) | ||
this.addResultProperty(result, sh.focusNode, focusNode) | ||
this.addResultPropertyDeep(result, sh.sourceShape, sourceShape) | ||
this.addResultPropertyDeep(result, sh.focusNode, focusNode) | ||
if (valueNode) { | ||
this.addResultProperty(result, sh.value, valueNode) | ||
this.addResultPropertyDeep(result, sh.value, valueNode) | ||
} | ||
@@ -41,2 +42,11 @@ return result | ||
addResultPropertyDeep (result, predicate, node) { | ||
this.addResultProperty(result, predicate, node) | ||
const structureQuads = extractStructure(this.context.$shapes.dataset, node) | ||
for (const quad of structureQuads) { | ||
this.results.push(quad) | ||
} | ||
} | ||
/** | ||
@@ -58,3 +68,3 @@ * Creates all the validation result nodes and messages for the result of applying the validation logic | ||
if (constraint.shape.isPropertyShape()) { | ||
this.addResultProperty(result, sh.resultPath, constraint.shape.path) // TODO: Make deep copy | ||
this.addResultPropertyDeep(result, sh.resultPath, constraint.shape.path, true) | ||
} | ||
@@ -69,3 +79,3 @@ this.createResultMessages(result, constraint) | ||
if (constraint.shape.isPropertyShape()) { | ||
this.addResultProperty(result, sh.resultPath, constraint.shape.path) // TODO: Make deep copy | ||
this.addResultPropertyDeep(result, sh.resultPath, constraint.shape.path, true) | ||
} | ||
@@ -81,10 +91,10 @@ this.addResultProperty(result, sh.resultMessage, this.factory.literal(obj, xsd.string)) | ||
if (obj.path) { | ||
this.addResultProperty(result, sh.resultPath, obj.path) // TODO: Make deep copy | ||
this.addResultPropertyDeep(result, sh.resultPath, obj.path, true) | ||
} else if (constraint.shape.isPropertyShape()) { | ||
this.addResultProperty(result, sh.resultPath, constraint.shape.path) // TODO: Make deep copy | ||
this.addResultPropertyDeep(result, sh.resultPath, constraint.shape.path, true) | ||
} | ||
if (obj.value) { | ||
this.addResultProperty(result, sh.value, obj.value) | ||
this.addResultPropertyDeep(result, sh.value, obj.value) | ||
} else if (valueNode) { | ||
this.addResultProperty(result, sh.value, valueNode) | ||
this.addResultPropertyDeep(result, sh.value, valueNode) | ||
} | ||
@@ -91,0 +101,0 @@ if (obj.message) { |
const { validateTerm } = require('rdf-validate-datatype') | ||
const NodeSet = require('./node-set') | ||
const { rdf, sh } = require('./namespaces') | ||
const { getPathObjects } = require('./property-path') | ||
@@ -60,13 +61,12 @@ function validateAnd (context, focusNode, valueNode, constraint) { | ||
const disjointNode = constraint.getParameterValue(sh.disjoint) | ||
return !context.$data.hasMatch(focusNode, disjointNode, valueNode) | ||
return context.$data.match(focusNode, disjointNode, valueNode).size === 0 | ||
} | ||
function validateEqualsProperty (context, focusNode, valueNode, constraint) { | ||
const pathNode = constraint.shape.path | ||
const path = constraint.shape.pathObject | ||
const equalsNode = constraint.getParameterValue(sh.equals) | ||
const results = [] | ||
const path = toRDFQueryPath(context.$shapes, pathNode) | ||
context.$data.getPathObjects(focusNode, path).forEach(value => { | ||
if (!context.$data.hasMatch(focusNode, equalsNode, value)) { | ||
getPathObjects(context.$data, focusNode, path).forEach(value => { | ||
if (context.$data.match(focusNode, equalsNode, value).size === 0) { | ||
results.push({ value }) | ||
@@ -79,3 +79,3 @@ } | ||
const value = object | ||
if (!context.$data.getPathObjects(focusNode, path).some(pathValue => pathValue.equals(value))) { | ||
if (!getPathObjects(context.$data, focusNode, path).some(pathValue => pathValue.equals(value))) { | ||
results.push({ value }) | ||
@@ -93,3 +93,3 @@ } | ||
let solutions = 0 | ||
context.$data.getPathObjects(focusNode, equalsNode).forEach(value => { | ||
getPathObjects(context.$data, focusNode, equalsNode).forEach(value => { | ||
solutions++ | ||
@@ -114,8 +114,6 @@ if (compareNodes(focusNode, value) !== 0) { | ||
function validateHasValueProperty (context, focusNode, valueNode, constraint) { | ||
const pathNode = constraint.shape.path | ||
const path = toRDFQueryPath(context.$shapes, pathNode) | ||
const path = constraint.shape.pathObject | ||
const hasValueNode = constraint.getParameterValue(sh.hasValue) | ||
return context.$data | ||
.getPathObjects(focusNode, path) | ||
return getPathObjects(context.$data, focusNode, path) | ||
.some(value => value.equals(hasValueNode)) | ||
@@ -146,5 +144,4 @@ } | ||
function validateLessThanProperty (context, focusNode, valueNode, constraint) { | ||
const pathNode = constraint.shape.path | ||
const valuePath = toRDFQueryPath(context.$shapes, pathNode) | ||
const values = context.$data.getPathObjects(focusNode, valuePath) | ||
const valuePath = constraint.shape.pathObject | ||
const values = getPathObjects(context.$data, focusNode, valuePath) | ||
const lessThanNode = constraint.getParameterValue(sh.lessThan) | ||
@@ -166,5 +163,4 @@ const referenceValues = context.$data.cf.node(focusNode).out(lessThanNode).terms | ||
function validateLessThanOrEqualsProperty (context, focusNode, valueNode, constraint) { | ||
const pathNode = constraint.shape.path | ||
const valuePath = toRDFQueryPath(context.$shapes, pathNode) | ||
const values = context.$data.getPathObjects(focusNode, valuePath) | ||
const valuePath = constraint.shape.pathObject | ||
const values = getPathObjects(context.$data, focusNode, valuePath) | ||
const lessThanOrEqualsNode = constraint.getParameterValue(sh.lessThanOrEquals) | ||
@@ -186,5 +182,4 @@ const referenceValues = context.$data.cf.node(focusNode).out(lessThanOrEqualsNode).terms | ||
function validateMaxCountProperty (context, focusNode, valueNode, constraint) { | ||
const pathNode = constraint.shape.path | ||
const path = toRDFQueryPath(context.$shapes, pathNode) | ||
const count = context.$data.getPathObjects(focusNode, path).length | ||
const path = constraint.shape.pathObject | ||
const count = getPathObjects(context.$data, focusNode, path).length | ||
const maxCountNode = constraint.getParameterValue(sh.maxCount) | ||
@@ -215,5 +210,4 @@ | ||
function validateMinCountProperty (context, focusNode, valueNode, constraint) { | ||
const pathNode = constraint.shape.path | ||
const path = toRDFQueryPath(context.$shapes, pathNode) | ||
const count = context.$data.getPathObjects(focusNode, path).length | ||
const path = constraint.shape.pathObject | ||
const count = getPathObjects(context.$data, focusNode, path).length | ||
const minCountNode = constraint.getParameterValue(sh.minCount) | ||
@@ -324,6 +318,4 @@ | ||
const pathNode = constraint.shape.path | ||
const path = toRDFQueryPath(context.$shapes, pathNode) | ||
return context.$data | ||
.getPathObjects(focusNode, path) | ||
const path = constraint.shape.pathObject | ||
return getPathObjects(context.$data, focusNode, path) | ||
.filter(value => | ||
@@ -352,6 +344,5 @@ context.nodeConformsToShape(value, qualifiedValueShapeNode) && | ||
const pathNode = constraint.shape.path | ||
const path = toRDFQueryPath(context.$shapes, pathNode) | ||
const path = constraint.shape.pathObject | ||
const map = {} | ||
context.$data.getPathObjects(focusNode, path).forEach(value => { | ||
getPathObjects(context.$data, focusNode, path).forEach(value => { | ||
const lang = value.language | ||
@@ -391,50 +382,2 @@ if (lang && lang !== '') { | ||
// Utilities ------------------------------------------------------------------ | ||
function toRDFQueryPath ($shapes, shPath) { | ||
if (shPath.termType === 'NamedNode') { | ||
return shPath | ||
} | ||
if (shPath.termType === 'BlankNode') { | ||
const shPathCf = $shapes.cf.node(shPath) | ||
const first = shPathCf.out(rdf.first).term | ||
if (first) { | ||
const paths = $shapes.rdfListToArray(shPath) | ||
return paths.map(path => toRDFQueryPath($shapes, path)) | ||
} | ||
const alternativePath = shPathCf.out(sh.alternativePath).term | ||
if (alternativePath) { | ||
const paths = $shapes.rdfListToArray(alternativePath) | ||
return { or: paths.map(path => toRDFQueryPath($shapes, path)) } | ||
} | ||
const zeroOrMorePath = shPathCf.out(sh.zeroOrMorePath).term | ||
if (zeroOrMorePath) { | ||
return { zeroOrMore: toRDFQueryPath($shapes, zeroOrMorePath) } | ||
} | ||
const oneOrMorePath = shPathCf.out(sh.oneOrMorePath).term | ||
if (oneOrMorePath) { | ||
return { oneOrMore: toRDFQueryPath($shapes, oneOrMorePath) } | ||
} | ||
const zeroOrOnePath = shPathCf.out(sh.zeroOrOnePath).term | ||
if (zeroOrOnePath) { | ||
return { zeroOrOne: toRDFQueryPath($shapes, zeroOrOnePath) } | ||
} | ||
const inversePath = shPathCf.out(sh.inversePath).term | ||
if (inversePath) { | ||
return { inverse: toRDFQueryPath($shapes, inversePath) } | ||
} | ||
} | ||
throw new Error('Unsupported SHACL path ' + shPath) | ||
// TODO: implement conforming to AbstractQuery.path syntax | ||
// return shPath | ||
} | ||
// Private helper functions | ||
@@ -487,3 +430,2 @@ | ||
module.exports = { | ||
toRDFQueryPath, | ||
validateAnd, | ||
@@ -490,0 +432,0 @@ validateClass, |
395117
18
8262
91
Updatedclownface@^0.12.3