diff --git a/shacl12-core/index.html b/shacl12-core/index.html
index 0c130e0d..354d4af2 100644
--- a/shacl12-core/index.html
+++ b/shacl12-core/index.html
@@ -2520,6 +2520,7 @@
Node Expressions
@@ -2806,7 +2807,7 @@ Conformance Checking
has been reported by it.
- Conformance checking produces true
if and only if a given focus node
+ Conformance checking produces true
if and only if a given focus node
conforms to a given shape, and false
otherwise.
@@ -6074,6 +6075,62 @@
sh:reifierShape, sh:reificationRequired
+
+
+ sh:nodeByExpression
+
+ sh:nodeByExpression
specifies the condition that each value node conforms to the
+ node shapes produced by a node expression.
+ The evaluation of these node expressions is repeated for all value nodes of the shape
+ as the focus node.
+
+
+ Constraint Component IRI: sh:NodeByExpressionConstraintComponent
+
+
+ Parameters:
+
+
+
+
+Let
$expr
be a
value of
sh:nodeByExpression
.
+For each
value node v
: perform a
conformance check of
+
v
against each
output node of
evalExpr(expr,
+data graph, v, {})
s
. A
failure
+MUST be produced if the
conformance check of
v
against
+
s
produces a
failure. Otherwise, if
v
does
+not
conform to
s
, there is a
validation result
+with
v
as
sh:value
and a
deep copy of
+
s
as
sh:sourceConstraint
.
+
+
+ The remainder of this section is informative.
+
+ sh:nodeByExpression
functions similarly to sh:node
, but instead of referencing a fixed node shape,
+ a referenced node expression is used to dynamically compute the set of node shapes to which each value node must conform.
+
+
+ Note that `sh:node` and `sh:nodeByExpression` exhibit the same behavior when given a value that is an IRI of a node shape.
+ In this case, `sh:node` directly validates against the specified node shape, whereas `sh:nodeByExpression` interprets the IRI
+ as an IRI expression that evaluates to a set containing the same node shape.
+
+
+ For a simple example of its use, refer to the example for sh:node
,
+ replacing `sh:node` with `sh:nodeByExpression`.
+
+
@@ -6975,6 +7032,7 @@ Changes between SHACL 1.0 Core and SHACL 1.2 Core
Added the new class sh:ShapeClass
for implicit class targets; see Issue 212
Moved SPARQL-based validators from Core to an Appendix of SHACL-SPARQL; see Issue 271
Added the new constraint component sh:expression
; see Issue 357
+ Added the new constraint component sh:nodeByExpression
, see Issue 408
Added the new value sh:ByTypes
for sh:closed
; see Issue 172
The values of sh:class
and sh:datatype
can now also be lists, indicating a union of choices; see Issue 160
diff --git a/shacl12-test-suite/tests/core/node/manifest.ttl b/shacl12-test-suite/tests/core/node/manifest.ttl
index 59b174b3..16be05e8 100644
--- a/shacl12-test-suite/tests/core/node/manifest.ttl
+++ b/shacl12-test-suite/tests/core/node/manifest.ttl
@@ -32,6 +32,7 @@
mf:include ;
mf:include ;
mf:include ;
+ mf:include ;
mf:include ;
mf:include ;
mf:include ;
diff --git a/shacl12-test-suite/tests/core/node/nodeByExpression-001.ttl b/shacl12-test-suite/tests/core/node/nodeByExpression-001.ttl
new file mode 100644
index 00000000..b687e6ce
--- /dev/null
+++ b/shacl12-test-suite/tests/core/node/nodeByExpression-001.ttl
@@ -0,0 +1,59 @@
+@prefix dash: .
+@prefix ex: .
+@prefix mf: .
+@prefix owl: .
+@prefix rdf: .
+@prefix rdfs: .
+@prefix sh: .
+@prefix sht: .
+@prefix xsd: .
+
+ex:InvalidInstance
+ rdf:type ex:TestClass ;
+ rdfs:label "Invalid instance" ;
+.
+ex:TestClass
+ rdf:type rdfs:Class ;
+ rdf:type sh:NodeShape ;
+ rdfs:label "Test class" ;
+ rdfs:subClassOf rdfs:Resource ;
+ # Only using an IRI Expression here because Core doesn't define interesting node expressions
+ sh:nodeByExpression ex:TestNodeShape ;
+.
+ex:TestNodeShape
+ rdf:type sh:NodeShape ;
+ sh:class ex:OtherClass ;
+.
+ex:ValidInstance
+ rdf:type ex:OtherClass ;
+ rdf:type ex:TestClass ;
+ rdfs:label "Valid instance" ;
+.
+<>
+ rdf:type mf:Manifest ;
+ mf:entries (
+
+ ) ;
+.
+
+ rdf:type sht:Validate ;
+ rdfs:label "Test of sh:nodeByExpression at node shape 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:InvalidInstance ;
+ sh:resultSeverity sh:Violation ;
+ sh:sourceConstraint ex:TestNodeShape ;
+ sh:sourceConstraintComponent sh:NodeConstraintComponent ;
+ sh:sourceShape ex:TestClass ;
+ sh:value ex:InvalidInstance ;
+ ] ;
+ ] ;
+ mf:status sht:approved ;
+.
diff --git a/shacl12-test-suite/tests/core/property/manifest.ttl b/shacl12-test-suite/tests/core/property/manifest.ttl
index 2ab26040..50991ab0 100644
--- a/shacl12-test-suite/tests/core/property/manifest.ttl
+++ b/shacl12-test-suite/tests/core/property/manifest.ttl
@@ -33,6 +33,7 @@
mf:include ;
mf:include ;
mf:include ;
+ mf:include ;
mf:include ;
mf:include ;
mf:include ;
diff --git a/shacl12-test-suite/tests/core/property/nodeByExpression-001.ttl b/shacl12-test-suite/tests/core/property/nodeByExpression-001.ttl
new file mode 100644
index 00000000..9de6e0df
--- /dev/null
+++ b/shacl12-test-suite/tests/core/property/nodeByExpression-001.ttl
@@ -0,0 +1,117 @@
+@prefix dash: .
+@prefix ex: .
+@prefix mf: .
+@prefix owl: .
+@prefix rdf: .
+@prefix rdfs: .
+@prefix sh: .
+@prefix sht: .
+@prefix xsd: .
+
+ex:Anon
+ rdf:type ex:Person ;
+ ex:firstName "Anon" ;
+.
+ex:AssignedToShape
+ rdf:type sh:NodeShape ;
+ rdfs:comment "All assignees must have an email and a last name." ;
+ sh:property [
+ sh:path ex:email ;
+ sh:maxCount 1 ;
+ sh:minCount 1 ;
+ ] ;
+ sh:property [
+ sh:path ex:lastName ;
+ sh:maxCount 1 ;
+ sh:minCount 1 ;
+ ] ;
+.
+ex:Issue
+ rdf:type rdfs:Class ;
+ rdf:type sh:NodeShape ;
+ rdfs:label "Issue" ;
+ rdfs:subClassOf rdfs:Resource ;
+ sh:property ex:Issue-assignedTo ;
+ sh:property ex:Issue-submittedBy ;
+.
+ex:Issue-assignedTo
+ sh:path ex:assignedTo ;
+ sh:class ex:Person ;
+ # Only using an IRI Expression here because Core doesn't define interesting node expressions
+ sh:nodeByExpression ex:AssignedToShape ;
+.
+ex:Issue-submittedBy
+ sh:path ex:submittedBy ;
+ sh:class ex:Person ;
+ sh:minCount 1 ;
+.
+ex:Issue_1
+ rdf:type ex:Issue ;
+ ex:assignedTo ex:Anon ;
+ ex:submittedBy ex:Anon ;
+ rdfs:label "Issue 1" ;
+.
+ex:Issue_2
+ rdf:type ex:Issue ;
+ ex:assignedTo ex:JohnDoeWithEmail ;
+ ex:submittedBy ex:Anon ;
+ rdfs:label "Issue 2" ;
+.
+ex:JohnDoeWithEmail
+ rdf:type ex:Person ;
+ ex:email "john@doe.com" ;
+ ex:firstName "John" ;
+ ex:lastName "Doe" ;
+.
+ex:Person
+ rdf:type rdfs:Class ;
+ rdf:type sh:NodeShape ;
+ rdfs:label "Person" ;
+ rdfs:subClassOf rdfs:Resource ;
+ sh:property [
+ sh:path ex:email ;
+ ex:datatype xsd:string ;
+ rdfs:label "email" ;
+ ] ;
+ sh:property [
+ sh:path ex:firstName ;
+ rdfs:label "first name" ;
+ sh:datatype xsd:string ;
+ sh:maxCount 1 ;
+ sh:minCount 1 ;
+ ] ;
+ sh:property [
+ sh:path ex:lastName ;
+ rdfs:label "last name" ;
+ sh:datatype xsd:string ;
+ ] ;
+.
+<>
+ rdf:type mf:Manifest ;
+ mf:entries (
+
+ ) ;
+.
+
+ rdf:type sht:Validate ;
+ rdfs:label "Test of sh:nodeByExpression at property shape 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:Issue_1 ;
+ sh:resultPath ex:assignedTo ;
+ sh:resultSeverity sh:Violation ;
+ sh:sourceConstraint ex:AssignedToShape ;
+ sh:sourceConstraintComponent sh:NodeConstraintComponent ;
+ sh:sourceShape ex:Issue-assignedTo ;
+ sh:value ex:Anon ;
+ ] ;
+ ] ;
+ mf:status sht:approved ;
+.
diff --git a/shacl12-vocabularies/shacl.ttl b/shacl12-vocabularies/shacl.ttl
index 67ee87b3..d889b971 100644
--- a/shacl12-vocabularies/shacl.ttl
+++ b/shacl12-vocabularies/shacl.ttl
@@ -915,6 +915,26 @@ sh:node
rdfs:isDefinedBy sh: .
+sh:NodeByExpressionConstraintComponent
+ a sh:ConstraintComponent ;
+ rdfs:label "Node by expression constraint component"@en ;
+ rdfs:comment "A constraint component that can be used to verify that all value nodes conform to the node shape(s) produced by the given node expression."@en ;
+ sh:parameter sh:NodeByExpressionConstraintComponent-nodeByExpression ;
+ rdfs:isDefinedBy sh: .
+
+sh:NodeByExpressionConstraintComponent-nodeByExpression
+ a sh:Parameter ;
+ sh:path sh:nodeByExpression ;
+ rdfs:isDefinedBy sh: .
+
+sh:nodeByExpression
+ a rdf:Property ;
+ rdfs:label "node by expression"@en ;
+ rdfs:comment "Links a shape to node expressions, indicating the node shape(s) that all value nodes must conform to."@en ;
+ # rdfs:range sh:NodeShape ; (node expression)
+ rdfs:isDefinedBy sh: .
+
+
sh:NodeKindConstraintComponent
a sh:ConstraintComponent ;
rdfs:label "Node-kind constraint component"@en ;