Skip to content

Commit

Permalink
fix: using property shapes in logical constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
tpluscode committed Aug 6, 2024
1 parent 378f3db commit f49bdc0
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/selfish-pants-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"rdf-validate-shacl": patch
---

Gracefully handle Property Shapes used inside logical constraints (fixes #140)
17 changes: 14 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,19 @@ class SHACLValidator {
}

// Exposed to be available from validation functions as `SHACL.nodeConformsToShape`
nodeConformsToShape(focusNode, shapeNode, engine = this.validationEngine.clone()) {
const shape = this.shapesGraph.getShape(shapeNode)
nodeConformsToShape(focusNode, shapeNode, propertyPathOrEngine) {
let engine
let shape = this.shapesGraph.getShape(shapeNode)

if (propertyPathOrEngine && 'termType' in propertyPathOrEngine) {
engine = this.validationEngine.clone({
propertyPath: propertyPathOrEngine,
recordErrorsLevel: this.validationEngine.recordErrorsLevel,
})
shape = shape.overridePath(propertyPathOrEngine)
} else {
engine = propertyPathOrEngine || this.validationEngine.clone()
}
try {
this.depth++
const foundViolations = engine.validateNodeAgainstShape(focusNode, shape, this.$data)
Expand All @@ -78,7 +89,7 @@ class SHACLValidator {
}
}

validateNodeAgainstShape (focusNode, shapeNode) {
validateNodeAgainstShape(focusNode, shapeNode) {
return this.nodeConformsToShape(focusNode, shapeNode, this.validationEngine)
}
}
Expand Down
15 changes: 14 additions & 1 deletion src/shapes-graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ class Constraint {
return this.paramValue || this.shapeNodePointer.out(param).term
}

get pathObject() {
return this.shape.pathObject
}

get validationFunction() {
return this.shape.isPropertyShape
? this.component.propertyValidationFunction
Expand Down Expand Up @@ -231,7 +235,6 @@ class Shape {
this.severity = this.shapeNodePointer.out(sh.severity).term || sh.Violation
this.deactivated = this.shapeNodePointer.out(sh.deactivated).value === 'true'
this.path = this.shapeNodePointer.out(sh.path).term
this.isPropertyShape = this.path != null
this._pathObject = undefined

this.constraints = []
Expand All @@ -251,6 +254,16 @@ class Shape {
})
}

get isPropertyShape() {
return this.path != null
}

overridePath(path) {
const shape = new Shape(this.context, this.shapeNode)
shape.path = path
return shape
}

/**
* Property path object
*/
Expand Down
14 changes: 10 additions & 4 deletions src/validation-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,21 @@ class ValidationEngine {
this.factory = context.factory
this.maxErrors = options.maxErrors
this.maxNodeChecks = options.maxNodeChecks === undefined ? defaultMaxNodeChecks : options.maxNodeChecks
this.propertyPath = options.propertyPath
this.initReport()
this.recordErrorsLevel = 0
this.recordErrorsLevel = options.recordErrorsLevel || 0
this.violationsCount = 0
this.validationError = null
this.nestedResults = {}
this.nestedResults = options.nestedResults || {}
}

clone() {
return new ValidationEngine(this.context, { maxErrors: this.maxErrors, maxNodeChecks: this.maxNodeChecks })
clone({ propertyPath, recordErrorsLevel } = {}) {
return new ValidationEngine(this.context, {
maxErrors: this.maxErrors,
maxNodeChecks: this.maxNodeChecks,
propertyPath,
recordErrorsLevel,
})
}

initReport() {
Expand Down
10 changes: 8 additions & 2 deletions src/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ function validateAnd(context, focusNode, valueNode, constraint) {
const andNode = constraint.getParameterValue(sh.and)
const shapes = rdfListToArray(context.$shapes.node(andNode))

return shapes.every((shape) => context.nodeConformsToShape(valueNode, shape))
return shapes.every((shape) => {
if (constraint.shape.isPropertyShape) {
return context.nodeConformsToShape(focusNode, shape, constraint.pathObject)
}

return context.nodeConformsToShape(valueNode, shape)
})
}

function validateClass(context, focusNode, valueNode, constraint) {
Expand Down Expand Up @@ -226,7 +232,7 @@ function validateMaxLength(context, focusNode, valueNode, constraint) {

function validateMinCountProperty(context, focusNode, valueNode, constraint) {
const { sh } = context.ns
const path = constraint.shape.pathObject
const path = constraint.pathObject
const count = getPathObjects(context.$data, focusNode, path).length
const minCountNode = constraint.getParameterValue(sh.minCount)

Expand Down
76 changes: 76 additions & 0 deletions test/data/data-shapes/custom/and-minCount.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
PREFIX schema: <http://schema.org/>
@prefix dash: <http://datashapes.org/dash#> .
@prefix ex: <http://datashapes.org/sh/tests/core/property/uniqueLang-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#> .

<>
rdf:type mf:Manifest ;
mf:entries
(
<and-minCount>
) ;
.
<and-minCount>
rdf:type sht:Validate ;
rdfs:label "Test of unexpected validation error as reported in issue zazuko/rdf-validate-shacl#140" ;
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:Instance ;
sh:resultPath schema:age ;
sh:resultSeverity sh:Violation ;
sh:sourceConstraintComponent sh:AndConstraintComponent ;
sh:sourceShape ex:age ;
sh:value 18 ;
],
[
rdf:type sh:ValidationResult ;
sh:focusNode ex:Instance ;
sh:resultPath schema:name ;
sh:resultSeverity sh:Violation ;
sh:sourceConstraintComponent sh:AndConstraintComponent ;
sh:sourceShape ex:name ;
sh:value "John" ;
] ;
] ;
mf:status sht:proposed ;
.
ex:Instance
schema:age 18 ;
schema:name "John" ;
a schema:Person ;
.
ex:PersonAddressShape
a sh:NodeShape ;
sh:targetClass schema:Person ;
sh:property ex:name, ex:age .

ex:ps1 a sh:PropertyShape ;
sh:minCount 2 ;
.

ex:name a sh:PropertyShape ;
sh:path schema:name ;
sh:and ( ex:ps1 ) ;
.

ex:age a sh:PropertyShape ;
sh:path schema:age ;
sh:and ( ex:ps1 ) ;
.
1 change: 1 addition & 0 deletions test/data/data-shapes/custom/manifest.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
mf:include <targetNodeDoesNotExist.ttl> ;
mf:include <lessThan-moreTypes.ttl> ;
mf:include <circularReferences.ttl> ;
mf:include <and-minCount.ttl> ;
.

0 comments on commit f49bdc0

Please sign in to comment.