shacl 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +24 -0
- data/README.md +159 -0
- data/VERSION +1 -0
- data/etc/doap.ttl +35 -0
- data/lib/rdf/vocab/shacl.rb +2073 -0
- data/lib/shacl.rb +93 -0
- data/lib/shacl/algebra.rb +34 -0
- data/lib/shacl/algebra/and.rb +51 -0
- data/lib/shacl/algebra/node_shape.rb +80 -0
- data/lib/shacl/algebra/not.rb +30 -0
- data/lib/shacl/algebra/operator.rb +329 -0
- data/lib/shacl/algebra/or.rb +46 -0
- data/lib/shacl/algebra/property_shape.rb +190 -0
- data/lib/shacl/algebra/qualified_value_shape.rb +47 -0
- data/lib/shacl/algebra/shape.rb +499 -0
- data/lib/shacl/algebra/xone.rb +65 -0
- data/lib/shacl/context.rb +41 -0
- data/lib/shacl/format.rb +88 -0
- data/lib/shacl/refinements.rb +198 -0
- data/lib/shacl/shapes.rb +160 -0
- data/lib/shacl/validation_report.rb +109 -0
- data/lib/shacl/validation_result.rb +153 -0
- data/lib/shacl/version.rb +19 -0
- metadata +217 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
module SHACL::Algebra
|
2
|
+
##
|
3
|
+
class Or < Operator
|
4
|
+
NAME = :or
|
5
|
+
|
6
|
+
##
|
7
|
+
# Specifies the condition that each value node conforms to at least one of the provided shapes. This is comparable to disjunction and the logical "or" operator.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# ex:OrConstraintExampleShape
|
11
|
+
# a sh:NodeShape ;
|
12
|
+
# sh:targetNode ex:Bob ;
|
13
|
+
# sh:or (
|
14
|
+
# [
|
15
|
+
# sh:path ex:firstName ;
|
16
|
+
# sh:minCount 1 ;
|
17
|
+
# ]
|
18
|
+
# [
|
19
|
+
# sh:path ex:givenName ;
|
20
|
+
# sh:minCount 1 ;
|
21
|
+
# ]
|
22
|
+
# ) .
|
23
|
+
#
|
24
|
+
# @param [RDF::Term] node
|
25
|
+
# @param [Hash{Symbol => Object}] options
|
26
|
+
# @return [Array<SHACL::ValidationResult>]
|
27
|
+
def conforms(node, path: nil, depth: 0, **options)
|
28
|
+
log_debug(NAME, depth: depth) {SXP::Generator.string({node: node}.to_sxp_bin)}
|
29
|
+
operands.each do |op|
|
30
|
+
results = op.conforms(node, depth: depth + 1, **options)
|
31
|
+
next unless results.all?(&:conform?)
|
32
|
+
return satisfy(focus: node, path: path,
|
33
|
+
value: node,
|
34
|
+
message: "node conforms to some shape",
|
35
|
+
component: RDF::Vocab::SHACL.OrConstraintComponent,
|
36
|
+
depth: depth, **options)
|
37
|
+
end
|
38
|
+
return not_satisfied(focus: node, path: path,
|
39
|
+
value: node,
|
40
|
+
message: "node does not conform to any shape",
|
41
|
+
resultSeverity: options.fetch(:severity),
|
42
|
+
component: RDF::Vocab::SHACL.OrConstraintComponent,
|
43
|
+
depth: depth, **options)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
require_relative "shape"
|
2
|
+
|
3
|
+
module SHACL::Algebra
|
4
|
+
##
|
5
|
+
class PropertyShape < Shape
|
6
|
+
NAME = :PropertyShape
|
7
|
+
|
8
|
+
# Validates the specified `property` within `graph`, a list of {ValidationResult}.
|
9
|
+
#
|
10
|
+
# A property conforms the nodes found by evaluating it's `path` all conform.
|
11
|
+
#
|
12
|
+
# @param [RDF::Term] node
|
13
|
+
# @param [Hash{Symbol => Object}] options
|
14
|
+
# @return [Array<SHACL::ValidationResult>]
|
15
|
+
# Returns a validation result for each value node.
|
16
|
+
def conforms(node, depth: 0, **options)
|
17
|
+
return [] if deactivated?
|
18
|
+
options = id ? options.merge(shape: id) : options
|
19
|
+
options = options.merge(severity: RDF::Vocab::SHACL.Violation)
|
20
|
+
|
21
|
+
# Add some instance options to the argument
|
22
|
+
options = %i{
|
23
|
+
flags
|
24
|
+
qualifiedMinCount
|
25
|
+
qualifiedMaxCount
|
26
|
+
qualifiedValueShapesDisjoint
|
27
|
+
severity
|
28
|
+
}.inject(options) do |memo, sym|
|
29
|
+
@options[sym] ? memo.merge(sym => @options[sym]) : memo
|
30
|
+
end
|
31
|
+
|
32
|
+
path = @options[:path]
|
33
|
+
log_debug(NAME, depth: depth) {SXP::Generator.string({id: id, node: node, path: path}.to_sxp_bin)}
|
34
|
+
log_error(NAME, "no path", depth: depth) unless path
|
35
|
+
|
36
|
+
# Turn the `path` attribute into a SPARQL Property Path and evaluate to find related nodes.
|
37
|
+
value_nodes = if path.is_a?(RDF::URI)
|
38
|
+
graph.query(subject: node, predicate: path).objects
|
39
|
+
elsif path.evaluatable?
|
40
|
+
path.execute(graph,
|
41
|
+
subject: node,
|
42
|
+
object: RDF::Query::Variable.new(:object)).map do
|
43
|
+
|soln| soln[:object]
|
44
|
+
end.compact.uniq
|
45
|
+
else
|
46
|
+
log_error(NAME, "Can't handle path", depth: depth) {path.to_sxp}
|
47
|
+
[]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Evaluate against builtins
|
51
|
+
builtin_results = @options.map do |k, v|
|
52
|
+
self.send("builtin_#{k}".to_sym, v, node, path, value_nodes, depth: depth + 1, **options) if self.respond_to?("builtin_#{k}".to_sym)
|
53
|
+
end.flatten.compact
|
54
|
+
|
55
|
+
# Evaluate against operands
|
56
|
+
op_results = operands.map do |op|
|
57
|
+
if op.is_a?(QualifiedValueShape)
|
58
|
+
# All value nodes are passed
|
59
|
+
op.conforms(node, value_nodes: value_nodes, path: path, depth: depth + 1, **options)
|
60
|
+
else
|
61
|
+
value_nodes.map do |n|
|
62
|
+
res = op.conforms(n, path: path, depth: depth + 1, **options)
|
63
|
+
if op.is_a?(NodeShape) && !res.all?(&:conform?)
|
64
|
+
# Special case for embedded NodeShape
|
65
|
+
not_satisfied(focus: node, path: path,
|
66
|
+
value: n,
|
67
|
+
message: "node does not conform to #{op.id}",
|
68
|
+
resultSeverity: options.fetch(:severity),
|
69
|
+
component: RDF::Vocab::SHACL.NodeConstraintComponent,
|
70
|
+
**options)
|
71
|
+
else
|
72
|
+
res
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end.flatten.compact
|
77
|
+
|
78
|
+
builtin_results + op_results
|
79
|
+
end
|
80
|
+
|
81
|
+
# The path defined on this property shape
|
82
|
+
# @return [RDF::URI, ]
|
83
|
+
def path
|
84
|
+
@options[:path]
|
85
|
+
end
|
86
|
+
|
87
|
+
# Specifies the condition that each value node is smaller than all the objects of the triples that have the focus node as subject and the value of sh:lessThan as predicate.
|
88
|
+
#
|
89
|
+
# @example
|
90
|
+
# ex:LessThanExampleShape
|
91
|
+
# a sh:NodeShape ;
|
92
|
+
# sh:property [
|
93
|
+
# sh:path ex:startDate ;
|
94
|
+
# sh:lessThan ex:endDate ;
|
95
|
+
# ] .
|
96
|
+
#
|
97
|
+
# @param [RDF::URI] property the property of the focus node whose values must be equal to some value node.
|
98
|
+
# @param [RDF::Term] node the focus node
|
99
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
100
|
+
# @param [Array<RDF::Term>] value_nodes
|
101
|
+
# @return [Array<SHACL::ValidationResult>]
|
102
|
+
def builtin_lessThan(property, node, path, value_nodes, **options)
|
103
|
+
terms = graph.query(subject: node, predicate: property).objects
|
104
|
+
compare(:<, terms, node, path, value_nodes,
|
105
|
+
RDF::Vocab::SHACL.LessThanConstraintComponent, **options)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Specifies the condition that each value node is smaller than or equal to all the objects of the triples that have the focus node as subject and the value of sh:lessThanOrEquals as predicate.
|
109
|
+
#
|
110
|
+
# @param [RDF::URI] property the property of the focus node whose values must be equal to some value node.
|
111
|
+
# @param [RDF::Term] node the focus node
|
112
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
113
|
+
# @param [Array<RDF::Term>] value_nodes
|
114
|
+
# @return [Array<SHACL::ValidationResult>]
|
115
|
+
def builtin_lessThanOrEquals(property, node, path, value_nodes, **options)
|
116
|
+
terms = graph.query(subject: node, predicate: property).objects
|
117
|
+
compare(:<=, terms, node, path, value_nodes,
|
118
|
+
RDF::Vocab::SHACL.LessThanOrEqualsConstraintComponent, **options)
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# Builin evaluators
|
123
|
+
##
|
124
|
+
|
125
|
+
# Specifies the maximum number of value nodes.
|
126
|
+
#
|
127
|
+
# @param [Integer] count
|
128
|
+
# @param [RDF::Term] node the focus node
|
129
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
130
|
+
# @param [Array<RDF::Term>] value_nodes
|
131
|
+
# @return [Array<SHACL::ValidationResult>]
|
132
|
+
def builtin_maxCount(count, node, path, value_nodes, **options)
|
133
|
+
satisfy(focus: node, path: path,
|
134
|
+
message: "#{value_nodes.count} <= maxCount #{count}",
|
135
|
+
resultSeverity: (options.fetch(:severity) unless value_nodes.count <= count.to_i),
|
136
|
+
component: RDF::Vocab::SHACL.MaxCountConstraintComponent,
|
137
|
+
**options)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Specifies the minimum number of value nodes.
|
141
|
+
#
|
142
|
+
# @example
|
143
|
+
# ex:MinCountExampleShape
|
144
|
+
# a sh:PropertyShape ;
|
145
|
+
# sh:targetNode ex:Alice, ex:Bob ;
|
146
|
+
# sh:path ex:name ;
|
147
|
+
# sh:minCount 1 .
|
148
|
+
#
|
149
|
+
# @param [Integer] count
|
150
|
+
# @param [RDF::Term] node the focus node
|
151
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
152
|
+
# @param [Array<RDF::Term>] value_nodes
|
153
|
+
# @return [Array<SHACL::ValidationResult>]
|
154
|
+
def builtin_minCount(count, node, path, value_nodes, **options)
|
155
|
+
satisfy(focus: node, path: path,
|
156
|
+
message: "#{value_nodes.count} >= minCount #{count}",
|
157
|
+
resultSeverity: (options.fetch(:severity) unless value_nodes.count >= count.to_i),
|
158
|
+
component: RDF::Vocab::SHACL.MinCountConstraintComponent,
|
159
|
+
**options)
|
160
|
+
end
|
161
|
+
|
162
|
+
# The property `sh:uniqueLang` can be set to `true` to specify that no pair of value nodes may use the same language tag.
|
163
|
+
#
|
164
|
+
# @param [Boolean] uniq
|
165
|
+
# @param [RDF::Term] node the focus node
|
166
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
167
|
+
# @param [Array<RDF::Term>] value_nodes
|
168
|
+
# @return [Array<SHACL::ValidationResult>]
|
169
|
+
def builtin_uniqueLang(uniq, node, path, value_nodes, **options)
|
170
|
+
if !value_nodes.all?(&:literal?)
|
171
|
+
not_satisfied(focus: node, path: path,
|
172
|
+
message: "not all values are literals",
|
173
|
+
resultSeverity: options.fetch(:severity),
|
174
|
+
component: RDF::Vocab::SHACL.UniqueLangConstraintComponent,
|
175
|
+
**options)
|
176
|
+
elsif value_nodes.map(&:language).compact.length != value_nodes.map(&:language).compact.uniq.length
|
177
|
+
not_satisfied(focus: node, path: path,
|
178
|
+
message: "not all values have unique language tags",
|
179
|
+
resultSeverity: options.fetch(:severity),
|
180
|
+
component: RDF::Vocab::SHACL.UniqueLangConstraintComponent,
|
181
|
+
**options)
|
182
|
+
else
|
183
|
+
satisfy(focus: node, path: path,
|
184
|
+
message: "all literals have unique language tags",
|
185
|
+
component: RDF::Vocab::SHACL.UniqueLangConstraintComponent,
|
186
|
+
**options)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module SHACL::Algebra
|
2
|
+
##
|
3
|
+
class QualifiedValueShape < Operator
|
4
|
+
NAME = :qualifiedValueShape
|
5
|
+
|
6
|
+
##
|
7
|
+
# Specifies the condition that a specified number of value nodes conforms to the given shape. Each `sh:qualifiedValueShape` can have: one value for `sh:qualifiedMinCount`, one value for s`h:qualifiedMaxCount` or, one value for each, at the same subject.
|
8
|
+
#
|
9
|
+
# @param [Array<RDF::Term>] value_nodes
|
10
|
+
# @param [Hash{Symbol => Object}] options
|
11
|
+
# @return [Array<SHACL::ValidationResult>]
|
12
|
+
def conforms(node, path:, value_nodes:, depth: 0, **options)
|
13
|
+
log_debug(NAME, depth: depth) {SXP::Generator.string({node: node, value_nodes: value_nodes}.to_sxp_bin)}
|
14
|
+
max_count = options.fetch(:qualifiedMaxCount, 0).to_i
|
15
|
+
min_count = options.fetch(:qualifiedMinCount, 0).to_i
|
16
|
+
# FIXME: figure this out
|
17
|
+
disjoint = options[:qualifiedValueShapesDisjoint]
|
18
|
+
|
19
|
+
operands.map do |op|
|
20
|
+
results = value_nodes.map do |n|
|
21
|
+
op.conforms(n, depth: depth + 1, **options)
|
22
|
+
end.flatten.compact
|
23
|
+
|
24
|
+
count = results.select(&:conform?).length
|
25
|
+
log_debug(NAME, depth: depth) {"#{count}/#{results} conforming shapes"}
|
26
|
+
if count < min_count
|
27
|
+
not_satisfied(focus: node, path: path,
|
28
|
+
message: "only #{count} conforming values, requires at least #{min_count}",
|
29
|
+
resultSeverity: options.fetch(:severity),
|
30
|
+
component: RDF::Vocab::SHACL.QualifiedMinCountConstraintComponent,
|
31
|
+
depth: depth, **options)
|
32
|
+
elsif count > max_count
|
33
|
+
not_satisfied(focus: node, path: path,
|
34
|
+
message: "#{count} conforming values, requires at most #{max_count}",
|
35
|
+
resultSeverity: options.fetch(:severity),
|
36
|
+
component: RDF::Vocab::SHACL.QualifiedMaxCountConstraintComponent,
|
37
|
+
depth: depth, **options)
|
38
|
+
else
|
39
|
+
satisfy(focus: node, path: path,
|
40
|
+
message: "#{min_count} <= #{count} <= #{max_count} values conform",
|
41
|
+
component: RDF::Vocab::SHACL.QualifiedMinCountConstraintComponent,
|
42
|
+
depth: depth, **options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,499 @@
|
|
1
|
+
module SHACL::Algebra
|
2
|
+
##
|
3
|
+
class Shape < Operator
|
4
|
+
NAME = :Shape
|
5
|
+
|
6
|
+
##
|
7
|
+
# Returns the nodes matching this particular shape, based upon the shape properties:
|
8
|
+
# * `targetNode`
|
9
|
+
# * `targetClass`
|
10
|
+
# * `targetSubjectsOf`
|
11
|
+
# * `targetObjectsOf`
|
12
|
+
# * `id` – where `type` includes `rdfs:Class`
|
13
|
+
#
|
14
|
+
# @return [Array<RDF::Term>]
|
15
|
+
def targetNodes
|
16
|
+
(Array(@options[:targetNode]) +
|
17
|
+
Array(@options[:targetClass]).map do |cls|
|
18
|
+
graph.query(predicate: RDF.type, object: cls).subjects
|
19
|
+
end +
|
20
|
+
Array(@options[:targetSubjectsOf]).map do |pred|
|
21
|
+
graph.query(predicate: pred).subjects
|
22
|
+
end +
|
23
|
+
Array(@options[:targetObjectsOf]).map do |pred|
|
24
|
+
graph.query(predicate: pred).objects
|
25
|
+
end + (
|
26
|
+
Array(type).include?(RDF::RDFS.Class) ?
|
27
|
+
graph.query(predicate: RDF.type, object: id).subjects :
|
28
|
+
[]
|
29
|
+
)).flatten.uniq
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Builin evaluators. These evaulators may be used on either NodeShapes or PropertyShapes.
|
34
|
+
##
|
35
|
+
|
36
|
+
# Specifies that each value node is a SHACL instance of a given type.
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# ex:ClassExampleShape
|
40
|
+
# a sh:NodeShape ;
|
41
|
+
# sh:targetNode ex:Bob, ex:Alice, ex:Carol ;
|
42
|
+
# sh:property [
|
43
|
+
# sh:path ex:address ;
|
44
|
+
# sh:class ex:PostalAddress ;
|
45
|
+
# ] .
|
46
|
+
#
|
47
|
+
# @param [Array<RDF::URI>] types The type expected for each value node.
|
48
|
+
# @param [RDF::Term] node the focus node
|
49
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
50
|
+
# @param [Array<RDF::Term>] value_nodes
|
51
|
+
# @return [Array<SHACL::ValidationResult>]
|
52
|
+
def builtin_class(types, node, path, value_nodes, **options)
|
53
|
+
value_nodes.map do |n|
|
54
|
+
has_type = n.resource? && begin
|
55
|
+
objects = graph.query(subject: n, predicate: RDF.type).objects
|
56
|
+
types.all? {|t| objects.include?(t)}
|
57
|
+
end
|
58
|
+
satisfy(focus: node, path: path,
|
59
|
+
value: n,
|
60
|
+
message: "is#{' not' unless has_type} of class #{type.to_sxp}",
|
61
|
+
resultSeverity: (options.fetch(:severity) unless has_type),
|
62
|
+
component: RDF::Vocab::SHACL.ClassConstraintComponent,
|
63
|
+
**options)
|
64
|
+
end.flatten.compact
|
65
|
+
end
|
66
|
+
|
67
|
+
# Specifies a condition to be satisfied with regards to the datatype of each value node.
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# ex:DatatypeExampleShape
|
71
|
+
# a sh:NodeShape ;
|
72
|
+
# sh:targetNode ex:Alice, ex:Bob, ex:Carol ;
|
73
|
+
# sh:property [
|
74
|
+
# sh:path ex:age ;
|
75
|
+
# sh:datatype xsd:integer ;
|
76
|
+
# ] .
|
77
|
+
#
|
78
|
+
#
|
79
|
+
# @param [RDF::URI] datatype the expected datatype of each value node.
|
80
|
+
# @param [RDF::Term] node the focus node
|
81
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
82
|
+
# @return [Array<SHACL::ValidationResult>]
|
83
|
+
def builtin_datatype(datatype, node, path, value_nodes, **options)
|
84
|
+
value_nodes.map do |n|
|
85
|
+
has_datatype = n.literal? && n.datatype == datatype && n.valid?
|
86
|
+
satisfy(focus: node, path: path,
|
87
|
+
value: n,
|
88
|
+
message: "is#{' not' unless has_datatype} a valid literal with datatype #{datatype.to_sxp}",
|
89
|
+
resultSeverity: (options.fetch(:severity) unless has_datatype),
|
90
|
+
component: RDF::Vocab::SHACL.DatatypeConstraintComponent,
|
91
|
+
**options)
|
92
|
+
end.flatten.compact
|
93
|
+
end
|
94
|
+
|
95
|
+
# Specifies the condition that the set of value nodes is disjoint with the set of objects of the triples that have the focus node as subject and the value of sh:disjoint as predicate.
|
96
|
+
#
|
97
|
+
# @example
|
98
|
+
# ex:DisjointExampleShape
|
99
|
+
# a sh:NodeShape ;
|
100
|
+
# sh:targetNode ex:USA, ex:Germany ;
|
101
|
+
# sh:property [
|
102
|
+
# sh:path ex:prefLabel ;
|
103
|
+
# sh:disjoint ex:altLabel ;
|
104
|
+
# ] .
|
105
|
+
#
|
106
|
+
# @param [Array<RDF::URI>] properties the properties of the focus node whose values must be disjoint with the value nodes.
|
107
|
+
# @param [RDF::Term] node the focus node
|
108
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
109
|
+
# @param [Array<RDF::Term>] value_nodes
|
110
|
+
# @return [Array<SHACL::ValidationResult>]
|
111
|
+
def builtin_disjoint(properties, node, path, value_nodes, **options)
|
112
|
+
disjoint_nodes = properties.map do |prop|
|
113
|
+
graph.query(subject: node, predicate: prop).objects
|
114
|
+
end.flatten.compact
|
115
|
+
value_nodes.map do |n|
|
116
|
+
has_value = disjoint_nodes.include?(n)
|
117
|
+
satisfy(focus: node, path: path,
|
118
|
+
value: n,
|
119
|
+
message: "is#{' not' unless has_value} disjoint with #{disjoint_nodes.to_sxp}",
|
120
|
+
resultSeverity: (options.fetch(:severity) if has_value),
|
121
|
+
component: RDF::Vocab::SHACL.DisjointConstraintComponent,
|
122
|
+
**options)
|
123
|
+
end.flatten.compact
|
124
|
+
end
|
125
|
+
|
126
|
+
# Specifies the condition that the set of all value nodes is equal to the set of objects of the triples that have the focus node as subject and the value of sh:equals as predicate.
|
127
|
+
#
|
128
|
+
# @example
|
129
|
+
# ex:EqualExampleShape
|
130
|
+
# a sh:NodeShape ;
|
131
|
+
# sh:targetNode ex:Bob ;
|
132
|
+
# sh:property [
|
133
|
+
# sh:path ex:firstName ;
|
134
|
+
# sh:equals ex:givenName ;
|
135
|
+
# ] .
|
136
|
+
#
|
137
|
+
# @param [RDF::URI] property the property of the focus node whose values must be equal to some value node.
|
138
|
+
# @param [RDF::Term] node the focus node
|
139
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
140
|
+
# @param [Array<RDF::Term>] value_nodes
|
141
|
+
# @return [Array<SHACL::ValidationResult>]
|
142
|
+
def builtin_equals(property, node, path, value_nodes, **options)
|
143
|
+
equal_nodes = graph.query(subject: node, predicate: property).objects
|
144
|
+
(value_nodes.map do |n|
|
145
|
+
has_value = equal_nodes.include?(n)
|
146
|
+
satisfy(focus: node, path: path,
|
147
|
+
value: n,
|
148
|
+
message: "is#{' not' unless has_value} a value in #{equal_nodes.to_sxp}",
|
149
|
+
resultSeverity: (options.fetch(:severity) unless has_value),
|
150
|
+
component: RDF::Vocab::SHACL.EqualsConstraintComponent,
|
151
|
+
**options)
|
152
|
+
end +
|
153
|
+
equal_nodes.map do |n|
|
154
|
+
!value_nodes.include?(n) ?
|
155
|
+
not_satisfied(focus: node, path: path,
|
156
|
+
value: n,
|
157
|
+
message: "should have a value in #{value_nodes.to_sxp}",
|
158
|
+
resultSeverity: options.fetch(:severity),
|
159
|
+
component: RDF::Vocab::SHACL.EqualsConstraintComponent,
|
160
|
+
**options) :
|
161
|
+
nil
|
162
|
+
end).flatten.compact
|
163
|
+
end
|
164
|
+
|
165
|
+
# Specifies the condition that at least one value node is equal to the given RDF term.
|
166
|
+
#
|
167
|
+
# @example
|
168
|
+
# ex:StanfordGraduate
|
169
|
+
# a sh:NodeShape ;
|
170
|
+
# sh:targetNode ex:Alice ;
|
171
|
+
# sh:property [
|
172
|
+
# sh:path ex:alumniOf ;
|
173
|
+
# sh:hasValue ex:Stanford ;
|
174
|
+
# ] .
|
175
|
+
#
|
176
|
+
# @param [RDF::URI] term the term that must be a value of a value node.
|
177
|
+
# @param [RDF::Term] node the focus node
|
178
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
179
|
+
# @param [Array<RDF::Term>] value_nodes
|
180
|
+
# @return [Array<SHACL::ValidationResult>]
|
181
|
+
def builtin_hasValue(term, node, path, value_nodes, **options)
|
182
|
+
has_value = value_nodes.include?(term)
|
183
|
+
[satisfy(focus: node, path: path,
|
184
|
+
message: "is#{' not' unless has_value} the value #{term.to_sxp}",
|
185
|
+
resultSeverity: (options.fetch(:severity) unless has_value),
|
186
|
+
component: RDF::Vocab::SHACL.HasValueConstraintComponent,
|
187
|
+
**options)]
|
188
|
+
end
|
189
|
+
|
190
|
+
# Specifies the condition that each value node is a member of a provided SHACL list.
|
191
|
+
#
|
192
|
+
# @example
|
193
|
+
# ex:InExampleShape
|
194
|
+
# a sh:NodeShape ;
|
195
|
+
# sh:targetNode ex:RainbowPony ;
|
196
|
+
# sh:property [
|
197
|
+
# sh:path ex:color ;
|
198
|
+
# sh:in ( ex:Pink ex:Purple ) ;
|
199
|
+
# ] .
|
200
|
+
#
|
201
|
+
# @param [RDF::URI] list the list which must contain the value nodes..
|
202
|
+
# @param [RDF::Term] node the focus node
|
203
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
204
|
+
# @param [Array<RDF::Term>] value_nodes
|
205
|
+
# @return [Array<SHACL::ValidationResult>]
|
206
|
+
def builtin_in(list, node, path, value_nodes, **options)
|
207
|
+
value_nodes.map do |n|
|
208
|
+
has_value = list.include?(n)
|
209
|
+
satisfy(focus: node, path: path,
|
210
|
+
value: n,
|
211
|
+
message: "is#{' not' unless has_value} a value in #{list.to_sxp}",
|
212
|
+
resultSeverity: (options.fetch(:severity) unless has_value),
|
213
|
+
component: RDF::Vocab::SHACL.InConstraintComponent,
|
214
|
+
**options)
|
215
|
+
end.flatten.compact
|
216
|
+
end
|
217
|
+
|
218
|
+
# The condition specified by sh:languageIn is that the allowed language tags for each value node are limited by a given list of language tags.
|
219
|
+
#
|
220
|
+
# @example
|
221
|
+
# ex:NewZealandLanguagesShape
|
222
|
+
# a sh:NodeShape ;
|
223
|
+
# sh:targetNode ex:Mountain, ex:Berg ;
|
224
|
+
# sh:property [
|
225
|
+
# sh:path ex:prefLabel ;
|
226
|
+
# sh:languageIn ( "en" "mi" ) ;
|
227
|
+
# ] .
|
228
|
+
#
|
229
|
+
# @param [Array<RDF::URI>] datatypes the expected datatype of each value node.
|
230
|
+
# @param [RDF::Term] node the focus node
|
231
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
232
|
+
# @return [Array<SHACL::ValidationResult>]
|
233
|
+
def builtin_languageIn(datatypes, node, path, value_nodes, **options)
|
234
|
+
value_nodes.map do |n|
|
235
|
+
has_language = n.literal? && datatypes.any? {|l| n.language.to_s.start_with?(l)}
|
236
|
+
satisfy(focus: node, path: path,
|
237
|
+
value: n,
|
238
|
+
message: "is#{' not' unless has_language} a literal with a language in #{datatypes.to_sxp}",
|
239
|
+
resultSeverity: (options.fetch(:severity) unless has_language),
|
240
|
+
component: RDF::Vocab::SHACL.LanguageInConstraintComponent,
|
241
|
+
**options)
|
242
|
+
end.flatten.compact
|
243
|
+
end
|
244
|
+
|
245
|
+
# Compares value nodes to be < than the specified value.
|
246
|
+
#
|
247
|
+
# @example
|
248
|
+
# ex:NumericRangeExampleShape
|
249
|
+
# a sh:NodeShape ;
|
250
|
+
# sh:targetNode ex:Bob, ex:Alice, ex:Ted ;
|
251
|
+
# sh:property [
|
252
|
+
# sh:path ex:age ;
|
253
|
+
# sh:minInclusive 0 ;
|
254
|
+
# sh:maxInclusive 150 ;
|
255
|
+
# ] .
|
256
|
+
#
|
257
|
+
# @param [RDF::URI] term the term is used to compare each value node.
|
258
|
+
# @param [RDF::Term] node the focus node
|
259
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
260
|
+
# @param [Array<RDF::Term>] value_nodes
|
261
|
+
# @return [Array<SHACL::ValidationResult>]
|
262
|
+
def builtin_maxExclusive(term, node, path, value_nodes, **options)
|
263
|
+
compare(:<, [term], node, path, value_nodes,
|
264
|
+
RDF::Vocab::SHACL.MaxExclusiveConstraintComponent, **options)
|
265
|
+
end
|
266
|
+
|
267
|
+
# Compares value nodes to be <= than the specified value.
|
268
|
+
#
|
269
|
+
# @example
|
270
|
+
# ex:NumericRangeExampleShape
|
271
|
+
# a sh:NodeShape ;
|
272
|
+
# sh:targetNode ex:Bob, ex:Alice, ex:Ted ;
|
273
|
+
# sh:property [
|
274
|
+
# sh:path ex:age ;
|
275
|
+
# sh:minInclusive 0 ;
|
276
|
+
# sh:maxInclusive 150 ;
|
277
|
+
# ] .
|
278
|
+
#
|
279
|
+
# @param [RDF::URI] term the term is used to compare each value node.
|
280
|
+
# @param [RDF::Term] node the focus node
|
281
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
282
|
+
# @param [Array<RDF::Term>] value_nodes
|
283
|
+
# @return [Array<SHACL::ValidationResult>]
|
284
|
+
def builtin_maxInclusive(term, node, path, value_nodes, **options)
|
285
|
+
compare(:<=, [term], node, path, value_nodes,
|
286
|
+
RDF::Vocab::SHACL.MaxInclusiveConstraintComponent, **options)
|
287
|
+
end
|
288
|
+
|
289
|
+
# Specifies the maximum string length of each value node that satisfies the condition. This can be applied to any literals and IRIs, but not to blank nodes.
|
290
|
+
#
|
291
|
+
# @param [RDF::URI] term the term is used to compare each value node.
|
292
|
+
# @param [RDF::Term] node the focus node
|
293
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
294
|
+
# @param [Array<RDF::Term>] value_nodes
|
295
|
+
# @return [Array<SHACL::ValidationResult>]
|
296
|
+
def builtin_maxLength(term, node, path, value_nodes, **options)
|
297
|
+
value_nodes.map do |n|
|
298
|
+
compares = !n.node? && n.to_s.length <= term.to_i
|
299
|
+
satisfy(focus: node, path: path,
|
300
|
+
value: n,
|
301
|
+
message: "is#{' not' unless compares} a literal at with length <= #{term.to_sxp}",
|
302
|
+
resultSeverity: (options.fetch(:severity) unless compares),
|
303
|
+
component: RDF::Vocab::SHACL.MaxLengthConstraintComponent,
|
304
|
+
**options)
|
305
|
+
end.flatten.compact
|
306
|
+
end
|
307
|
+
|
308
|
+
# Compares value nodes to be > than the specified value.
|
309
|
+
#
|
310
|
+
# @example
|
311
|
+
# ex:NumericRangeExampleShape
|
312
|
+
# a sh:NodeShape ;
|
313
|
+
# sh:targetNode ex:Bob, ex:Alice, ex:Ted ;
|
314
|
+
# sh:property [
|
315
|
+
# sh:path ex:age ;
|
316
|
+
# sh:minInclusive 0 ;
|
317
|
+
# sh:maxInclusive 150 ;
|
318
|
+
# ] .
|
319
|
+
#
|
320
|
+
# @param [RDF::URI] term the term is used to compare each value node.
|
321
|
+
# @param [RDF::Term] node the focus node
|
322
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
323
|
+
# @param [Array<RDF::Term>] value_nodes
|
324
|
+
# @return [Array<SHACL::ValidationResult>]
|
325
|
+
def builtin_minExclusive(term, node, path, value_nodes, **options)
|
326
|
+
compare(:>, [term], node, path, value_nodes,
|
327
|
+
RDF::Vocab::SHACL.MinExclusiveConstraintComponent, **options)
|
328
|
+
end
|
329
|
+
|
330
|
+
# Compares value nodes to be >= than the specified value.
|
331
|
+
#
|
332
|
+
# @example
|
333
|
+
# ex:NumericRangeExampleShape
|
334
|
+
# a sh:NodeShape ;
|
335
|
+
# sh:targetNode ex:Bob, ex:Alice, ex:Ted ;
|
336
|
+
# sh:property [
|
337
|
+
# sh:path ex:age ;
|
338
|
+
# sh:minInclusive 0 ;
|
339
|
+
# sh:maxInclusive 150 ;
|
340
|
+
# ] .
|
341
|
+
#
|
342
|
+
# @param [RDF::URI] term the term is used to compare each value node.
|
343
|
+
# @param [RDF::Term] node the focus node
|
344
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus nod to the value nodes.
|
345
|
+
# @param [Array<RDF::Term>] value_nodes
|
346
|
+
# @return [Array<SHACL::ValidationResult>]
|
347
|
+
def builtin_minInclusive(term, node, path, value_nodes, **options)
|
348
|
+
compare(:>=, [term], node, path, value_nodes,
|
349
|
+
RDF::Vocab::SHACL.MinInclusiveConstraintComponent, **options)
|
350
|
+
end
|
351
|
+
|
352
|
+
# Specifies the minimum string length of each value node that satisfies the condition. This can be applied to any literals and IRIs, but not to blank nodes.
|
353
|
+
#
|
354
|
+
# @param [RDF::URI] term the term is used to compare each value node.
|
355
|
+
# @param [RDF::Term] node the focus node
|
356
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
357
|
+
# @param [Array<RDF::Term>] value_nodes
|
358
|
+
# @return [Array<SHACL::ValidationResult>]
|
359
|
+
def builtin_minLength(term, node, path, value_nodes, **options)
|
360
|
+
value_nodes.map do |n|
|
361
|
+
compares = !n.node? && n.to_s.length >= term.to_i
|
362
|
+
satisfy(focus: node, path: path,
|
363
|
+
value: n,
|
364
|
+
message: "is#{' not' unless compares} a literal with length >= #{term.to_sxp}",
|
365
|
+
resultSeverity: (options.fetch(:severity) unless compares),
|
366
|
+
component: RDF::Vocab::SHACL.MinLengthConstraintComponent,
|
367
|
+
**options)
|
368
|
+
end.flatten.compact
|
369
|
+
end
|
370
|
+
|
371
|
+
# The matrix of comparisons of different types of nodes
|
372
|
+
# @return {Hash{Class => RDF::URI}}
|
373
|
+
NODE_KIND_COMPARE = {
|
374
|
+
RDF::URI => [
|
375
|
+
RDF::Vocab::SHACL.IRI,
|
376
|
+
RDF::Vocab::SHACL.BlankNodeOrIRI,
|
377
|
+
RDF::Vocab::SHACL.IRIOrLiteral,
|
378
|
+
],
|
379
|
+
RDF::Node => [
|
380
|
+
RDF::Vocab::SHACL.BlankNode,
|
381
|
+
RDF::Vocab::SHACL.BlankNodeOrIRI,
|
382
|
+
RDF::Vocab::SHACL.BlankNodeOrLiteral,
|
383
|
+
],
|
384
|
+
RDF::Literal => [
|
385
|
+
RDF::Vocab::SHACL.Literal,
|
386
|
+
RDF::Vocab::SHACL.IRIOrLiteral,
|
387
|
+
RDF::Vocab::SHACL.BlankNodeOrLiteral,
|
388
|
+
]
|
389
|
+
}
|
390
|
+
|
391
|
+
# Specifies a condition to be satisfied by the RDF node kind of each value node.
|
392
|
+
#
|
393
|
+
# @example
|
394
|
+
# ex:NodeKindExampleShape
|
395
|
+
# a sh:NodeShape ;
|
396
|
+
# sh:targetObjectsOf ex:knows ;
|
397
|
+
# sh:nodeKind sh:IRI .
|
398
|
+
#
|
399
|
+
# @param [RDF::URI] term the kind of node to match each value node.
|
400
|
+
# @param [RDF::Term] node the focus node
|
401
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the to the value nodes.
|
402
|
+
# @param [Array<RDF::Term>] value_nodes
|
403
|
+
# @return [Array<SHACL::ValidationResult>]
|
404
|
+
def builtin_nodeKind(term, node, path, value_nodes, **options)
|
405
|
+
value_nodes.map do |n|
|
406
|
+
compares = NODE_KIND_COMPARE.fetch(n.class, []).include?(term)
|
407
|
+
satisfy(focus: node, path: path,
|
408
|
+
value: n,
|
409
|
+
message: "is#{' not' unless compares} a node kind match of #{term.to_sxp}",
|
410
|
+
resultSeverity: (options.fetch(:severity) unless compares),
|
411
|
+
component: RDF::Vocab::SHACL.NodeKindConstraintComponent,
|
412
|
+
**options)
|
413
|
+
end.flatten.compact
|
414
|
+
end
|
415
|
+
|
416
|
+
# Specifies a regular expression that each value node matches to satisfy the condition.
|
417
|
+
#
|
418
|
+
# @example
|
419
|
+
# ex:PatternExampleShape
|
420
|
+
# a sh:NodeShape ;
|
421
|
+
# sh:targetNode ex:Bob, ex:Alice, ex:Carol ;
|
422
|
+
# sh:property [
|
423
|
+
# sh:path ex:bCode ;
|
424
|
+
# sh:pattern "^B" ; # starts with 'B'
|
425
|
+
# sh:flags "i" ; # Ignore case
|
426
|
+
# ] .
|
427
|
+
#
|
428
|
+
# @param [RDF::URI] pattern A regular expression that all value nodes need to match.
|
429
|
+
# @param [RDF::Term] node the focus node
|
430
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes..
|
431
|
+
# @param [Array<RDF::Term>] value_nodes
|
432
|
+
# @return [Array<SHACL::ValidationResult>]
|
433
|
+
def builtin_pattern(pattern, node, path, value_nodes, **options)
|
434
|
+
flags = options[:flags].to_s
|
435
|
+
regex_opts = 0 |
|
436
|
+
regex_opts |= Regexp::MULTILINE if flags.include?(?m)
|
437
|
+
regex_opts |= Regexp::IGNORECASE if flags.include?(?i)
|
438
|
+
regex_opts |= Regexp::EXTENDED if flags.include?(?x)
|
439
|
+
pat = Regexp.new(pattern, regex_opts)
|
440
|
+
|
441
|
+
value_nodes.map do |n|
|
442
|
+
compares = !n.node? && pat.match?(n.to_s)
|
443
|
+
satisfy(focus: node, path: path,
|
444
|
+
value: n,
|
445
|
+
message: "is#{' not' unless compares} a match #{pat.inspect}",
|
446
|
+
resultSeverity: (options.fetch(:severity) unless compares),
|
447
|
+
component: RDF::Vocab::SHACL.PatternConstraintComponent,
|
448
|
+
**options)
|
449
|
+
end.flatten.compact
|
450
|
+
end
|
451
|
+
|
452
|
+
protected
|
453
|
+
|
454
|
+
# Common comparison logic for lessThan, lessThanOrEqual, max/minInclusive/Exclusive
|
455
|
+
def compare(method, terms, node, path, value_nodes, component, **options)
|
456
|
+
value_nodes.map do |left|
|
457
|
+
results = terms.map do |right|
|
458
|
+
case left
|
459
|
+
when RDF::Literal
|
460
|
+
unless right.literal? && (
|
461
|
+
(left.simple? && right.simple?) ||
|
462
|
+
(left.is_a?(RDF::Literal::Numeric) && right.is_a?(RDF::Literal::Numeric)) ||
|
463
|
+
(left.datatype == right.datatype && left.language == right.language))
|
464
|
+
:incomperable
|
465
|
+
else
|
466
|
+
left.send(method, right)
|
467
|
+
end
|
468
|
+
when RDF::URI
|
469
|
+
right.uri? && left.send(method, right)
|
470
|
+
else
|
471
|
+
:incomperable
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
if results.include?(:incomperable)
|
476
|
+
not_satisfied(focus: node, path: path,
|
477
|
+
value: left,
|
478
|
+
message: "is incomperable with #{terms.to_sxp}",
|
479
|
+
resultSeverity: options.fetch(:severity),
|
480
|
+
component: component,
|
481
|
+
**options)
|
482
|
+
elsif results.include?(false)
|
483
|
+
not_satisfied(focus: node, path: path,
|
484
|
+
value: left,
|
485
|
+
message: "is not #{method} than #{terms.to_sxp}",
|
486
|
+
resultSeverity: options.fetch(:severity),
|
487
|
+
component: component,
|
488
|
+
**options)
|
489
|
+
else
|
490
|
+
satisfy(focus: node, path: path,
|
491
|
+
value: left,
|
492
|
+
message: "is #{method} than #{terms.to_sxp}",
|
493
|
+
component: component,
|
494
|
+
**options)
|
495
|
+
end
|
496
|
+
end.flatten.compact
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|