From f49bdc06f8e68389d01cf9e9e504debce233e51b Mon Sep 17 00:00:00 2001 From: Tomasz Pluskiewicz Date: Tue, 6 Aug 2024 15:28:25 +0200 Subject: [PATCH] fix: using property shapes in logical constraints --- .changeset/selfish-pants-grin.md | 5 ++ index.js | 17 ++++- src/shapes-graph.js | 15 +++- src/validation-engine.js | 14 +++- src/validators.js | 10 ++- test/data/data-shapes/custom/and-minCount.ttl | 76 +++++++++++++++++++ test/data/data-shapes/custom/manifest.ttl | 1 + 7 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 .changeset/selfish-pants-grin.md create mode 100644 test/data/data-shapes/custom/and-minCount.ttl diff --git a/.changeset/selfish-pants-grin.md b/.changeset/selfish-pants-grin.md new file mode 100644 index 0000000..d6145d6 --- /dev/null +++ b/.changeset/selfish-pants-grin.md @@ -0,0 +1,5 @@ +--- +"rdf-validate-shacl": patch +--- + +Gracefully handle Property Shapes used inside logical constraints (fixes #140) diff --git a/index.js b/index.js index 52286e3..9dc1c8e 100644 --- a/index.js +++ b/index.js @@ -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) @@ -78,7 +89,7 @@ class SHACLValidator { } } - validateNodeAgainstShape (focusNode, shapeNode) { + validateNodeAgainstShape(focusNode, shapeNode) { return this.nodeConformsToShape(focusNode, shapeNode, this.validationEngine) } } diff --git a/src/shapes-graph.js b/src/shapes-graph.js index 270067b..0fde024 100644 --- a/src/shapes-graph.js +++ b/src/shapes-graph.js @@ -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 @@ -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 = [] @@ -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 */ diff --git a/src/validation-engine.js b/src/validation-engine.js index 49627d2..127347a 100644 --- a/src/validation-engine.js +++ b/src/validation-engine.js @@ -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() { diff --git a/src/validators.js b/src/validators.js index 918206d..5976952 100644 --- a/src/validators.js +++ b/src/validators.js @@ -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) { @@ -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) diff --git a/test/data/data-shapes/custom/and-minCount.ttl b/test/data/data-shapes/custom/and-minCount.ttl new file mode 100644 index 0000000..d45a952 --- /dev/null +++ b/test/data/data-shapes/custom/and-minCount.ttl @@ -0,0 +1,76 @@ +PREFIX schema: +@prefix dash: . +@prefix ex: . +@prefix mf: . +@prefix owl: . +@prefix rdf: . +@prefix rdfs: . +@prefix sh: . +@prefix sht: . +@prefix xsd: . + +<> + rdf:type mf:Manifest ; + mf:entries + ( + + ) ; +. + + 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 ) ; +. diff --git a/test/data/data-shapes/custom/manifest.ttl b/test/data/data-shapes/custom/manifest.ttl index 53a4352..0133e22 100644 --- a/test/data/data-shapes/custom/manifest.ttl +++ b/test/data/data-shapes/custom/manifest.ttl @@ -12,4 +12,5 @@ mf:include ; mf:include ; mf:include ; + mf:include ; .