shacl 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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