Skip to content

Commit

Permalink
feat: added support for SPARQLConstraint
Browse files Browse the repository at this point in the history
  • Loading branch information
bergos committed Feb 12, 2024
1 parent 1237d3d commit 7fd554e
Show file tree
Hide file tree
Showing 44 changed files with 1,971 additions and 5 deletions.
3 changes: 3 additions & 0 deletions lib/Result.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Result {
results = [],
severity,
shape,
source = [],
value,
valuePaths = []
} = {}) {
Expand All @@ -36,6 +37,7 @@ class Result {
this.results = results
this.severity = severity
this.shape = shape
this.source = source
this.value = value
this.valuePaths = valuePaths

Expand Down Expand Up @@ -66,6 +68,7 @@ class Result {
.addOut([ns.rdf.type], [ns.sh.ValidationResult])
.addOut([ns.sh.focusNode], this.focusNode.terms)
.addOut([ns.sh.resultSeverity], [this.severity])
.addOut([ns.sh.sourceConstraint], this.source)
.addOut([ns.sh.sourceConstraintComponent], [this.constraintComponent])
.addOut([ns.sh.sourceShape], this.shape.ptr.terms)

Expand Down
9 changes: 9 additions & 0 deletions lib/Shape.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ class Shape {
this._path = once(() => parsePath(this.ptr.out([ns.sh.path])))
this._severity = once(() => this.ptr.out([ns.sh.severity]).term)
this._shapeValidator = once(() => new ShapeValidator(this))
this._sparql = once(() => this.ptr.out([ns.sh.sparql]))
this._targetResolver = once(() => new TargetResolver(this.ptr))
}

get isPropertyShape () {
return Boolean(this.path)
}

get isSparqlShape () {
return this.sparql.terms.length > 0
}

get path () {
return this._path()
}
Expand All @@ -40,6 +45,10 @@ class Shape {
return this._shapeValidator()
}

get sparql () {
return this._sparql()
}

resolveTargets (context) {
return this.targetResolver.resolve(context)
}
Expand Down
10 changes: 9 additions & 1 deletion lib/ShapeValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,15 @@ class ShapeValidator {
}

async validateProperty (context) {
const resolved = context.focusNode.executeAll(this.shape.path)
let resolved

if (this.shape.isSparqlShape) {
// traversing happens in the SPARQL constraints
resolved = context.focusNode
} else {
resolved = context.focusNode.executeAll(this.shape.path)
}

const values = resolved.node(new TermSet(resolved.terms))
const valuesPaths = [...resolved].reduce((valuesPaths, valuePaths) => {
const term = valuePaths.term
Expand Down
2 changes: 2 additions & 0 deletions lib/namespaces.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import namespace from '@rdfjs/namespace'

const owl = namespace('http://www.w3.org/2002/07/owl#')
const rdf = namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#')
const rdfs = namespace('http://www.w3.org/2000/01/rdf-schema#')
const sh = namespace('http://www.w3.org/ns/shacl#')
const shn = namespace('https://schemas.link/shacl-next#')
const xsd = namespace('http://www.w3.org/2001/XMLSchema#')

export {
owl,
rdf,
rdfs,
sh,
Expand Down
70 changes: 70 additions & 0 deletions lib/sparql.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { BindingsFactory } from '@comunica/bindings-factory'
import { QueryEngine } from '@comunica/query-sparql-rdfjs'
import { Readable } from 'readable-stream'

function bindingsToObject (row) {
const args = {}

for (const [key, value] of [...row]) {
args[key.value] = value
}

return args
}

async function select ({ bindings, dataset, factory, query }) {
const store = new DatasetSource(dataset)
const engine = new QueryEngine()

const bindingsFactory = new BindingsFactory(factory)
const initialBindings = bindingsFactory.bindings(bindings)

const stream = await engine.queryBindings(query, {
sources: [store],
initialBindings
})

return stream.toArray()
}

function stringifyPath (path) {
if (!path) {
return null
}

return path.map(pathStep => {
let sparqlPath = ''

if (pathStep.start === 'object' && pathStep.end === 'subject') {
sparqlPath += '^'
}

sparqlPath += pathStep.predicates.map(p => `<${p.value}>`).join('|')

if (pathStep.quantifier === 'oneOrMore') {
sparqlPath += '+'
} else if (pathStep.quantifier === 'zeroOrMore') {
sparqlPath += '*'
} else if (pathStep.quantifier === 'zeroOrOne') {
sparqlPath += '?'
}

return sparqlPath
}).join('/')
}

class DatasetSource {
constructor (dataset) {
this.dataset = dataset
}

match (subject, predicate, object, graph) {
return Readable.from(this.dataset.match(subject, predicate, object, graph))
}
}

export {
bindingsToObject,
select,
stringifyPath
}
5 changes: 4 additions & 1 deletion lib/validations.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { compileClosedNode, compileHasValue, compileIn } from './validations/oth
import { compileDisjoint, compileEquals, compileLessThan, compileLessThanOrEquals } from './validations/pair.js'
import { compileMaxExclusive, compileMaxInclusive, compileMinExclusive, compileMinInclusive } from './validations/range.js'
import { compileNode, compileProperty, compileQualifiedShape } from './validations/shape.js'
import { compileSparql } from './validations/sparql.js'
import { compileLanguageIn, compileMaxLength, compileMinLength, compilePattern, compileUniqueLang } from './validations/string.js'
import { compileTraversal } from './validations/traversal.js'
import { compileClass, compileDatatype, compileNodeKind } from './validations/type.js'
Expand Down Expand Up @@ -84,7 +85,9 @@ const registry = new Registry(new TermMap([

[ns.sh.class, compileClass],
[ns.sh.datatype, compileDatatype],
[ns.sh.nodeKind, compileNodeKind]
[ns.sh.nodeKind, compileNodeKind],

[ns.sh.sparql, compileSparql]
]))

export {
Expand Down
60 changes: 60 additions & 0 deletions lib/validations/sparql.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as ns from '../namespaces.js'
import parsePath from '../parsePath.js'
import { bindingsToObject, select, stringifyPath } from '../sparql.js'

function stringifyPrefixes (ptr, prefixes = []) {
for (const imports of ptr.out([ns.owl.imports])) {
stringifyPrefixes(imports, prefixes)
}

for (const declare of ptr.out([ns.sh.declare])) {
const prefix = declare.out([ns.sh.prefix]).value
const namespace = declare.out([ns.sh.namespace]).value

prefixes.push(`PREFIX ${prefix}: <${namespace}>`)
}

return prefixes
}

function compileSparql (shape) {
const select = shape.sparql.out([ns.sh.select]).value
const prefixes = stringifyPrefixes(shape.sparql.out([ns.sh.prefixes]))
const sparqlPath = stringifyPath(shape.path)
const message = shape.sparql.out([ns.sh.message]).terms

const query = [...prefixes, select]
.filter(Boolean)
.join('\n')
.split('$PATH')
.join(sparqlPath)

return {
generic: validateSparql({ message, query, source: shape.sparql.terms })
}
}

function validateSparql ({ message, query, source }) {
return async context => {
const dataset = context.focusNode.dataset
const factory = context.factory
const bindings = [[factory.variable('this'), context.focusNode.term]]
const rows = await select({ bindings, dataset, factory, query })

for (const row of rows) {
const args = bindingsToObject(row)
const path = row.has('path') && parsePath(context.focusNode.node([row.get('path')]))
const value = context.focusNode.node([row.get('value') || context.focusNode.term])

context.violation(ns.sh.SPARQLConstraintComponent, { args, message, path, source, value })
}

if (rows.length === 0) {
context.debug(ns.sh.SPARQLConstraintComponent, { source })
}
}
}

export {
compileSparql
}
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@
},
"homepage": "https://github.com/rdf-ext/shacl-engine",
"dependencies": {
"@comunica/bindings-factory": "^2.10.1",
"@comunica/query-sparql-rdfjs": "^2.10.1",
"@rdfjs/namespace": "^2.0.0",
"@rdfjs/term-map": "^2.0.0",
"@rdfjs/term-set": "^2.0.1",
"@rdfjs/to-ntriples": "^2.0.0",
"grapoi": "^1.1.1",
"lodash": "^4.17.21",
"rdf-literal": "^1.3.1",
"rdf-validation": "^0.1.0"
"rdf-validation": "^0.1.0",
"readable-stream": "^4.5.1"
},
"devDependencies": {
"@rdfjs/data-model": "^2.0.1",
Expand All @@ -43,7 +46,6 @@
"mocha": "^10.2.0",
"rdf-test": "^0.1.0",
"rdf-utils-fs": "^2.3.0",
"readable-stream": "^4.3.0",
"stream-chunks": "^1.0.0",
"stricter-standard": "^0.2.0"
}
Expand Down
1 change: 1 addition & 0 deletions test/assets/data-shapes/manifest.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
<>
a mf:Manifest ;
mf:include <core/manifest.ttl> ;
mf:include <sparql/manifest.ttl> ;
.
11 changes: 11 additions & 0 deletions test/assets/data-shapes/sparql/component/manifest.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@prefix mf: <http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sht: <http://www.w3.org/ns/shacl-test#> .

<>
a mf:Manifest ;
rdfs:label "Tests converted from http://datashapes.org/sh/tests/tests/sparql/component" ;
mf:include <optional-001.ttl> ;
mf:include <propertyValidator-select-001.ttl> ;
mf:include <validator-001.ttl> ;
.
83 changes: 83 additions & 0 deletions test/assets/data-shapes/sparql/component/nodeValidator-001.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
@prefix dash: <http://datashapes.org/dash#> .
@prefix ex: <http://datashapes.org/sh/tests/sparql/component/nodeValidator-001.test#> .
@prefix mf: <http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix sht: <http://www.w3.org/ns/shacl-test#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<http://datashapes.org/sh/tests/sparql/component/nodeValidator-001.test>
sh:declare [
rdf:type sh:PrefixDeclaration ;
sh:namespace "http://datashapes.org/sh/tests/sparql/component/nodeValidator-001.test#"^^xsd:anyURI ;
sh:prefix "ex" ;
] ;
.
ex:InvalidResource1
ex:property "Other" ;
.
ex:TestConstraintComponent
rdf:type sh:ConstraintComponent ;
rdfs:label "Test constraint component" ;
sh:nodeValidator [
rdf:type sh:SPARQLSelectValidator ;
sh:select """
SELECT DISTINCT $this
WHERE {
$this ?p ?o .
FILTER NOT EXISTS {
$this ex:property ?requiredParam .
}}""" ;
sh:prefixes <http://datashapes.org/sh/tests/sparql/component/nodeValidator-001.test> ;
] ;
sh:parameter [
sh:path ex:optionalParam ;
sh:datatype xsd:integer ;
sh:name "optional param" ;
sh:optional "true"^^xsd:boolean ;
] ;
sh:parameter [
sh:path ex:requiredParam ;
sh:datatype xsd:string ;
sh:name "required param" ;
] ;
.
ex:TestShape
rdf:type sh:NodeShape ;
ex:requiredParam "Value" ;
rdfs:label "Test shape" ;
sh:targetNode ex:InvalidResource1 ;
sh:targetNode ex:ValidResource1 ;
.
ex:ValidResource1
ex:property "Value" ;
.
<>
rdf:type mf:Manifest ;
mf:entries (
<nodeValidator-001>
) ;
.
<nodeValidator-001>
rdf:type sht:Validate ;
rdfs:label "Test of sh:nodeValidator 001" ;
mf:action [
sht:dataGraph <> ;
sht:shapesGraph <> ;
] ;
mf:result [
rdf:type sh:ValidationReport ;
sh:conforms "false"^^xsd:boolean ;
sh:result [
rdf:type sh:ValidationResult ;
sh:focusNode ex:InvalidResource1 ;
sh:resultSeverity sh:Violation ;
sh:sourceConstraintComponent ex:TestConstraintComponent ;
sh:sourceShape ex:TestShape ;
sh:value ex:InvalidResource1 ;
] ;
] ;
mf:status sht:proposed ;
.
Loading

0 comments on commit 7fd554e

Please sign in to comment.