shacl 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +11 -6
- data/VERSION +1 -1
- data/lib/rdf/vocab/shacl.rb +1096 -1096
- data/lib/shacl/algebra/and.rb +3 -2
- data/lib/shacl/algebra/constraint_component.rb +67 -0
- data/lib/shacl/algebra/node_shape.rb +10 -16
- data/lib/shacl/algebra/not.rb +3 -2
- data/lib/shacl/algebra/operator.rb +312 -44
- data/lib/shacl/algebra/or.rb +3 -2
- data/lib/shacl/algebra/pattern.rb +43 -0
- data/lib/shacl/algebra/property_shape.rb +11 -16
- data/lib/shacl/algebra/{qualified_value_shape.rb → qualified_value.rb} +14 -8
- data/lib/shacl/algebra/shape.rb +12 -41
- data/lib/shacl/algebra/sparql_constraint.rb +160 -0
- data/lib/shacl/algebra/xone.rb +3 -2
- data/lib/shacl/algebra.rb +30 -13
- data/lib/shacl/context.rb +5 -0
- data/lib/shacl/shapes.rb +99 -14
- data/lib/shacl/validation_result.rb +22 -19
- data/lib/shacl.rb +10 -5
- metadata +36 -19
@@ -9,25 +9,15 @@ module SHACL::Algebra
|
|
9
9
|
#
|
10
10
|
# A property conforms the nodes found by evaluating it's `path` all conform.
|
11
11
|
#
|
12
|
-
# @param [RDF::Term] node
|
12
|
+
# @param [RDF::Term] node focus node
|
13
13
|
# @param [Hash{Symbol => Object}] options
|
14
14
|
# @return [Array<SHACL::ValidationResult>]
|
15
15
|
# Returns a validation result for each value node.
|
16
16
|
def conforms(node, depth: 0, **options)
|
17
17
|
return [] if deactivated?
|
18
18
|
options = id ? options.merge(shape: id) : options
|
19
|
-
options = options
|
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
|
19
|
+
options[:severity] = @options[:severity] if @options[:severity]
|
20
|
+
options[:severity] ||= RDF::Vocab::SHACL.Violation
|
31
21
|
|
32
22
|
path = @options[:path]
|
33
23
|
log_debug(NAME, depth: depth) {SXP::Generator.string({id: id, node: node, path: path}.to_sxp_bin)}
|
@@ -54,9 +44,9 @@ module SHACL::Algebra
|
|
54
44
|
|
55
45
|
# Evaluate against operands
|
56
46
|
op_results = operands.map do |op|
|
57
|
-
if op.is_a?(
|
47
|
+
if op.is_a?(QualifiedValueConstraintComponent) || op.is_a?(SPARQLConstraintComponent)
|
58
48
|
# All value nodes are passed
|
59
|
-
op.conforms(node,
|
49
|
+
op.conforms(node, path: path, value_nodes: value_nodes, depth: depth + 1, **options)
|
60
50
|
else
|
61
51
|
value_nodes.map do |n|
|
62
52
|
res = op.conforms(n, path: path, depth: depth + 1, **options)
|
@@ -79,7 +69,7 @@ module SHACL::Algebra
|
|
79
69
|
end
|
80
70
|
|
81
71
|
# The path defined on this property shape
|
82
|
-
# @return [RDF::URI, ]
|
72
|
+
# @return [RDF::URI, SPARQL::Algebra::Expression]
|
83
73
|
def path
|
84
74
|
@options[:path]
|
85
75
|
end
|
@@ -100,6 +90,7 @@ module SHACL::Algebra
|
|
100
90
|
# @param [Array<RDF::Term>] value_nodes
|
101
91
|
# @return [Array<SHACL::ValidationResult>]
|
102
92
|
def builtin_lessThan(property, node, path, value_nodes, **options)
|
93
|
+
property = property.first if property.is_a?(Array)
|
103
94
|
terms = graph.query({subject: node, predicate: property}).objects
|
104
95
|
compare(:<, terms, node, path, value_nodes,
|
105
96
|
RDF::Vocab::SHACL.LessThanConstraintComponent, **options)
|
@@ -113,6 +104,7 @@ module SHACL::Algebra
|
|
113
104
|
# @param [Array<RDF::Term>] value_nodes
|
114
105
|
# @return [Array<SHACL::ValidationResult>]
|
115
106
|
def builtin_lessThanOrEquals(property, node, path, value_nodes, **options)
|
107
|
+
property = property.first if property.is_a?(Array)
|
116
108
|
terms = graph.query({subject: node, predicate: property}).objects
|
117
109
|
compare(:<=, terms, node, path, value_nodes,
|
118
110
|
RDF::Vocab::SHACL.LessThanOrEqualsConstraintComponent, **options)
|
@@ -130,6 +122,7 @@ module SHACL::Algebra
|
|
130
122
|
# @param [Array<RDF::Term>] value_nodes
|
131
123
|
# @return [Array<SHACL::ValidationResult>]
|
132
124
|
def builtin_maxCount(count, node, path, value_nodes, **options)
|
125
|
+
count = count.first if count.is_a?(Array)
|
133
126
|
satisfy(focus: node, path: path,
|
134
127
|
message: "#{value_nodes.count} <= maxCount #{count}",
|
135
128
|
resultSeverity: (options.fetch(:severity) unless value_nodes.count <= count.to_i),
|
@@ -152,6 +145,7 @@ module SHACL::Algebra
|
|
152
145
|
# @param [Array<RDF::Term>] value_nodes
|
153
146
|
# @return [Array<SHACL::ValidationResult>]
|
154
147
|
def builtin_minCount(count, node, path, value_nodes, **options)
|
148
|
+
count = count.first if count.is_a?(Array)
|
155
149
|
satisfy(focus: node, path: path,
|
156
150
|
message: "#{value_nodes.count} >= minCount #{count}",
|
157
151
|
resultSeverity: (options.fetch(:severity) unless value_nodes.count >= count.to_i),
|
@@ -167,6 +161,7 @@ module SHACL::Algebra
|
|
167
161
|
# @param [Array<RDF::Term>] value_nodes
|
168
162
|
# @return [Array<SHACL::ValidationResult>]
|
169
163
|
def builtin_uniqueLang(uniq, node, path, value_nodes, **options)
|
164
|
+
uniq = uniq.first if uniq.is_a?(Array)
|
170
165
|
if !value_nodes.all?(&:literal?)
|
171
166
|
not_satisfied(focus: node, path: path,
|
172
167
|
message: "not all values are literals",
|
@@ -1,35 +1,41 @@
|
|
1
1
|
module SHACL::Algebra
|
2
2
|
##
|
3
|
-
class
|
3
|
+
class QualifiedValueConstraintComponent < Operator
|
4
4
|
NAME = :qualifiedValueShape
|
5
5
|
|
6
6
|
##
|
7
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
8
|
#
|
9
|
+
# @param [RDF::Term] node focus node
|
10
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path the property path from the focus node to the value nodes.
|
9
11
|
# @param [Array<RDF::Term>] value_nodes
|
10
12
|
# @param [Hash{Symbol => Object}] options
|
11
13
|
# @return [Array<SHACL::ValidationResult>]
|
12
14
|
def conforms(node, path:, value_nodes:, depth: 0, **options)
|
13
15
|
log_debug(NAME, depth: depth) {SXP::Generator.string({node: node, value_nodes: value_nodes}.to_sxp_bin)}
|
14
|
-
|
15
|
-
|
16
|
+
# Separate operands into operators and parameters
|
17
|
+
params, ops = operands.partition {|o| o.is_a?(Array) && o.first.is_a?(Symbol)}
|
18
|
+
params = params.inject({}) {|memo, a| memo.merge(a.first => a.last)}
|
19
|
+
|
20
|
+
max_count = params[:qualifiedMinCount]
|
21
|
+
min_count = params[:qualifiedMinCount]
|
16
22
|
# FIXME: figure this out
|
17
|
-
disjoint =
|
23
|
+
disjoint = !!params[:qualifiedValueShapesDisjoint]
|
18
24
|
|
19
|
-
|
25
|
+
ops.map do |op|
|
20
26
|
results = value_nodes.map do |n|
|
21
27
|
op.conforms(n, depth: depth + 1, **options)
|
22
28
|
end.flatten.compact
|
23
29
|
|
24
30
|
count = results.select(&:conform?).length
|
25
31
|
log_debug(NAME, depth: depth) {"#{count}/#{results} conforming shapes"}
|
26
|
-
if count < min_count
|
32
|
+
if min_count && count < min_count.to_i
|
27
33
|
not_satisfied(focus: node, path: path,
|
28
34
|
message: "only #{count} conforming values, requires at least #{min_count}",
|
29
35
|
resultSeverity: options.fetch(:severity),
|
30
36
|
component: RDF::Vocab::SHACL.QualifiedMinCountConstraintComponent,
|
31
37
|
depth: depth, **options)
|
32
|
-
elsif count > max_count
|
38
|
+
elsif max_count && count > max_count.to_i
|
33
39
|
not_satisfied(focus: node, path: path,
|
34
40
|
message: "#{count} conforming values, requires at most #{max_count}",
|
35
41
|
resultSeverity: options.fetch(:severity),
|
@@ -37,7 +43,7 @@ module SHACL::Algebra
|
|
37
43
|
depth: depth, **options)
|
38
44
|
else
|
39
45
|
satisfy(focus: node, path: path,
|
40
|
-
message: "#{min_count} <= #{count} <= #{max_count} values conform",
|
46
|
+
message: "#{min_count.to_i} <= #{count} <= #{max_count || 'inf'} values conform",
|
41
47
|
component: RDF::Vocab::SHACL.QualifiedMinCountConstraintComponent,
|
42
48
|
depth: depth, **options)
|
43
49
|
end
|
data/lib/shacl/algebra/shape.rb
CHANGED
@@ -81,6 +81,7 @@ module SHACL::Algebra
|
|
81
81
|
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
82
82
|
# @return [Array<SHACL::ValidationResult>]
|
83
83
|
def builtin_datatype(datatype, node, path, value_nodes, **options)
|
84
|
+
datatype = datatype.first if datatype.is_a?(Array)
|
84
85
|
value_nodes.map do |n|
|
85
86
|
has_datatype = n.literal? && n.datatype == datatype && n.valid?
|
86
87
|
satisfy(focus: node, path: path,
|
@@ -140,6 +141,7 @@ module SHACL::Algebra
|
|
140
141
|
# @param [Array<RDF::Term>] value_nodes
|
141
142
|
# @return [Array<SHACL::ValidationResult>]
|
142
143
|
def builtin_equals(property, node, path, value_nodes, **options)
|
144
|
+
property = property.first if property.is_a?(Array)
|
143
145
|
equal_nodes = graph.query({subject: node, predicate: property}).objects
|
144
146
|
(value_nodes.map do |n|
|
145
147
|
has_value = equal_nodes.include?(n)
|
@@ -179,6 +181,7 @@ module SHACL::Algebra
|
|
179
181
|
# @param [Array<RDF::Term>] value_nodes
|
180
182
|
# @return [Array<SHACL::ValidationResult>]
|
181
183
|
def builtin_hasValue(term, node, path, value_nodes, **options)
|
184
|
+
term = term.first if term.is_a?(Array)
|
182
185
|
has_value = value_nodes.include?(term)
|
183
186
|
[satisfy(focus: node, path: path,
|
184
187
|
message: "is#{' not' unless has_value} the value #{term.to_sxp}",
|
@@ -260,7 +263,7 @@ module SHACL::Algebra
|
|
260
263
|
# @param [Array<RDF::Term>] value_nodes
|
261
264
|
# @return [Array<SHACL::ValidationResult>]
|
262
265
|
def builtin_maxExclusive(term, node, path, value_nodes, **options)
|
263
|
-
compare(:<,
|
266
|
+
compare(:<, term, node, path, value_nodes,
|
264
267
|
RDF::Vocab::SHACL.MaxExclusiveConstraintComponent, **options)
|
265
268
|
end
|
266
269
|
|
@@ -282,7 +285,7 @@ module SHACL::Algebra
|
|
282
285
|
# @param [Array<RDF::Term>] value_nodes
|
283
286
|
# @return [Array<SHACL::ValidationResult>]
|
284
287
|
def builtin_maxInclusive(term, node, path, value_nodes, **options)
|
285
|
-
compare(:<=,
|
288
|
+
compare(:<=, term, node, path, value_nodes,
|
286
289
|
RDF::Vocab::SHACL.MaxInclusiveConstraintComponent, **options)
|
287
290
|
end
|
288
291
|
|
@@ -294,6 +297,7 @@ module SHACL::Algebra
|
|
294
297
|
# @param [Array<RDF::Term>] value_nodes
|
295
298
|
# @return [Array<SHACL::ValidationResult>]
|
296
299
|
def builtin_maxLength(term, node, path, value_nodes, **options)
|
300
|
+
term = term.first if term.is_a?(Array)
|
297
301
|
value_nodes.map do |n|
|
298
302
|
compares = !n.node? && n.to_s.length <= term.to_i
|
299
303
|
satisfy(focus: node, path: path,
|
@@ -323,7 +327,7 @@ module SHACL::Algebra
|
|
323
327
|
# @param [Array<RDF::Term>] value_nodes
|
324
328
|
# @return [Array<SHACL::ValidationResult>]
|
325
329
|
def builtin_minExclusive(term, node, path, value_nodes, **options)
|
326
|
-
compare(:>,
|
330
|
+
compare(:>, term, node, path, value_nodes,
|
327
331
|
RDF::Vocab::SHACL.MinExclusiveConstraintComponent, **options)
|
328
332
|
end
|
329
333
|
|
@@ -341,11 +345,11 @@ module SHACL::Algebra
|
|
341
345
|
#
|
342
346
|
# @param [RDF::URI] term the term is used to compare each value node.
|
343
347
|
# @param [RDF::Term] node the focus node
|
344
|
-
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus
|
348
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
345
349
|
# @param [Array<RDF::Term>] value_nodes
|
346
350
|
# @return [Array<SHACL::ValidationResult>]
|
347
351
|
def builtin_minInclusive(term, node, path, value_nodes, **options)
|
348
|
-
compare(:>=,
|
352
|
+
compare(:>=, term, node, path, value_nodes,
|
349
353
|
RDF::Vocab::SHACL.MinInclusiveConstraintComponent, **options)
|
350
354
|
end
|
351
355
|
|
@@ -357,6 +361,7 @@ module SHACL::Algebra
|
|
357
361
|
# @param [Array<RDF::Term>] value_nodes
|
358
362
|
# @return [Array<SHACL::ValidationResult>]
|
359
363
|
def builtin_minLength(term, node, path, value_nodes, **options)
|
364
|
+
term = term.first if term.is_a?(Array)
|
360
365
|
value_nodes.map do |n|
|
361
366
|
compares = !n.node? && n.to_s.length >= term.to_i
|
362
367
|
satisfy(focus: node, path: path,
|
@@ -402,6 +407,7 @@ module SHACL::Algebra
|
|
402
407
|
# @param [Array<RDF::Term>] value_nodes
|
403
408
|
# @return [Array<SHACL::ValidationResult>]
|
404
409
|
def builtin_nodeKind(term, node, path, value_nodes, **options)
|
410
|
+
term = term.first if term.is_a?(Array)
|
405
411
|
value_nodes.map do |n|
|
406
412
|
compares = NODE_KIND_COMPARE.fetch(n.class, []).include?(term)
|
407
413
|
satisfy(focus: node, path: path,
|
@@ -413,46 +419,11 @@ module SHACL::Algebra
|
|
413
419
|
end.flatten.compact
|
414
420
|
end
|
415
421
|
|
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
422
|
protected
|
453
423
|
|
454
424
|
# Common comparison logic for lessThan, lessThanOrEqual, max/minInclusive/Exclusive
|
455
425
|
def compare(method, terms, node, path, value_nodes, component, **options)
|
426
|
+
terms = [terms] unless terms.is_a?(Array)
|
456
427
|
value_nodes.map do |left|
|
457
428
|
results = terms.map do |right|
|
458
429
|
case left
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require_relative "shape"
|
2
|
+
require 'sparql'
|
3
|
+
require 'rdf/aggregate_repo'
|
4
|
+
|
5
|
+
module SHACL::Algebra
|
6
|
+
##
|
7
|
+
class SPARQLConstraintComponent < ConstraintComponent
|
8
|
+
NAME = :sparql
|
9
|
+
|
10
|
+
# SPARQL Operators prohibited from being used in expression.
|
11
|
+
UNSUPPORTED_SPARQL_OPERATORS = [
|
12
|
+
SPARQL::Algebra::Operator::Minus,
|
13
|
+
SPARQL::Algebra::Operator::Service,
|
14
|
+
SPARQL::Algebra::Operator::Table,
|
15
|
+
]
|
16
|
+
|
17
|
+
# Potentially pre-bound variables.
|
18
|
+
PRE_BOUND = %i(currentShape shapesGraph PATH this value)
|
19
|
+
|
20
|
+
# Validates the specified `property` within `graph`, a list of {ValidationResult}.
|
21
|
+
#
|
22
|
+
# A property conforms the nodes found by evaluating it's `path` all conform.
|
23
|
+
#
|
24
|
+
# Last operand is the parsed query. Bound variables are added as a table entry joined to the query.
|
25
|
+
#
|
26
|
+
# @param [RDF::Term] node focus node
|
27
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path the property path from the focus node to the value nodes.
|
28
|
+
# @param [Hash{Symbol => Object}] options
|
29
|
+
# @return [Array<SHACL::ValidationResult>]
|
30
|
+
# Returns a validation result for each value node.
|
31
|
+
def conforms(node, path: nil, depth: 0, **options)
|
32
|
+
return [] if deactivated?
|
33
|
+
options = {severity: RDF::Vocab::SHACL.Violation}.merge(options)
|
34
|
+
log_debug(NAME, depth: depth) {SXP::Generator.string({node: node}.to_sxp_bin)}
|
35
|
+
|
36
|
+
# Aggregate repo containing both data-graph (as the default) and shapes-graph, named by it's IRI
|
37
|
+
aggregate = RDF::AggregateRepo.new(graph.data, shapes_graph.data) do |ag|
|
38
|
+
ag.default false
|
39
|
+
ag.named shapes_graph.graph_name if shapes_graph.graph_name
|
40
|
+
end
|
41
|
+
|
42
|
+
aggregate.default_graph
|
43
|
+
bindings = RDF::Query::Solution.new({
|
44
|
+
currentShape: options[:shape],
|
45
|
+
shapesGraph: shapes_graph.graph_name,
|
46
|
+
PATH: path,
|
47
|
+
this: node,
|
48
|
+
}.compact)
|
49
|
+
solutions = operands.last.execute(aggregate,
|
50
|
+
bindings: bindings,
|
51
|
+
depth: depth + 1,
|
52
|
+
logger: (@logger || @options[:logger]),
|
53
|
+
**options)
|
54
|
+
if solutions.empty?
|
55
|
+
satisfy(focus: node, path: path,
|
56
|
+
message: @options.fetch(:message, "node conforms to SPARQL component"),
|
57
|
+
component: RDF::Vocab::SHACL.SPARQLConstraintComponent,
|
58
|
+
depth: depth, **options)
|
59
|
+
else
|
60
|
+
solutions.map do |solution|
|
61
|
+
not_satisfied(focus: node, path: (path || solution[:path]),
|
62
|
+
value: (solution[:value] || node),
|
63
|
+
message: @options.fetch(:message, "node does not coform to SPARQL component"),
|
64
|
+
resultSeverity: options.fetch(:severity),
|
65
|
+
component: RDF::Vocab::SHACL.SPARQLConstraintComponent,
|
66
|
+
depth: depth, **options)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# All keys associated with shapes which are set in options
|
72
|
+
#
|
73
|
+
# @return [Array<Symbol>]
|
74
|
+
BUILTIN_KEYS = %i(
|
75
|
+
type label name comment description deactivated severity
|
76
|
+
message path
|
77
|
+
ask select
|
78
|
+
declare namespace prefix prefixes select ask
|
79
|
+
).freeze
|
80
|
+
|
81
|
+
# Class Methods
|
82
|
+
class << self
|
83
|
+
##
|
84
|
+
# Creates an operator instance from a parsed SHACL representation.
|
85
|
+
#
|
86
|
+
# Special case for SPARQLComponenet due to general recursion.
|
87
|
+
#
|
88
|
+
# @param [Hash] operator
|
89
|
+
# @param [Hash] options ({})
|
90
|
+
# @option options [Hash{String => RDF::URI}] :prefixes
|
91
|
+
# @return [Operator]
|
92
|
+
def from_json(operator, **options)
|
93
|
+
prefixes, query = [], ""
|
94
|
+
operands = []
|
95
|
+
node_opts = options.dup
|
96
|
+
operator.each do |k, v|
|
97
|
+
next if v.nil?
|
98
|
+
case k
|
99
|
+
# List properties
|
100
|
+
when 'path' then node_opts[:path] = parse_path(v, **options)
|
101
|
+
when 'prefixes'
|
102
|
+
prefixes = extract_prefixes(v)
|
103
|
+
when 'severity' then node_opts[:severity] = iri(v, **options)
|
104
|
+
when 'type' then node_opts[:type] = as_array(v).map {|vv| iri(vv, **options)} if v
|
105
|
+
else
|
106
|
+
node_opts[k.to_sym] = to_rdf(k.to_sym, v, **options) if BUILTIN_KEYS.include?(k.to_sym)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
query_string = prefixes.join("\n") + node_opts[:select] || node_opts[:ask]
|
111
|
+
query = SPARQL.parse(query_string)
|
112
|
+
|
113
|
+
options[:logger].info("#{NAME} SXP: #{query.to_sxp}") if options[:logger]
|
114
|
+
|
115
|
+
# Queries have restrictions
|
116
|
+
operators = query.descendants.to_a.unshift(query)
|
117
|
+
|
118
|
+
if node_opts[:ask] && !operators.any? {|op| op.is_a?(SPARQL::Algebra::Operator::Ask)}
|
119
|
+
raise SHACL::Error, "Ask query must have ask operator"
|
120
|
+
elsif node_opts[:select] && !operators.any? {|op| op.is_a?(SPARQL::Algebra::Operator::Project)}
|
121
|
+
raise SHACL::Error, "Select query must have project operator"
|
122
|
+
end
|
123
|
+
|
124
|
+
uh_oh = (operators.map(&:class) & UNSUPPORTED_SPARQL_OPERATORS).map {|c| c.const_get(:NAME)}
|
125
|
+
|
126
|
+
unless uh_oh.empty?
|
127
|
+
raise SHACL::Error, "Query must not include operators #{uh_oh.to_sxp}: #{query_string}"
|
128
|
+
end
|
129
|
+
|
130
|
+
# Additionally, queries must not bind to special variables
|
131
|
+
operators.select {|op| op.is_a?(SPARQL::Algebra::Operator::Extend)}.each do |extend|
|
132
|
+
if extend.operands.first.any? {|v, e| PRE_BOUND.include?(v.to_sym)}
|
133
|
+
raise SHACL::Error, "Query must not bind pre-bound variables: #{query_string}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
operands << query
|
138
|
+
new(*operands, **node_opts)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
# Returns an array of prefix definitions
|
145
|
+
def self.extract_prefixes(value)
|
146
|
+
case value
|
147
|
+
when Hash
|
148
|
+
ret = []
|
149
|
+
# Recursively extract decllarations
|
150
|
+
extract_prefixes(value.fetch('imports', nil)) +
|
151
|
+
as_array(value.fetch('declare', [])).map do |decl|
|
152
|
+
pfx, ns = decl['prefix'], decl['namespace']
|
153
|
+
"PREFIX #{pfx}: <#{ns}>"
|
154
|
+
end
|
155
|
+
when Array then value.map {|v| extract_prefixes(v)}.flatten
|
156
|
+
else []
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/lib/shacl/algebra/xone.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module SHACL::Algebra
|
2
2
|
##
|
3
|
-
class
|
3
|
+
class XoneConstraintComponent < ConstraintComponent
|
4
4
|
NAME = :xone
|
5
5
|
|
6
6
|
##
|
@@ -29,7 +29,8 @@ module SHACL::Algebra
|
|
29
29
|
# ]
|
30
30
|
# ) .
|
31
31
|
#
|
32
|
-
# @param [RDF::Term] node
|
32
|
+
# @param [RDF::Term] node focus node
|
33
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property
|
33
34
|
# @param [Hash{Symbol => Object}] options
|
34
35
|
# @return [Array<SHACL::ValidationResult>]
|
35
36
|
def conforms(node, path: nil, depth: 0, **options)
|
data/lib/shacl/algebra.rb
CHANGED
@@ -1,29 +1,46 @@
|
|
1
1
|
$:.unshift(File.expand_path("../..", __FILE__))
|
2
2
|
require 'sxp'
|
3
3
|
require_relative "algebra/operator"
|
4
|
+
require_relative "algebra/constraint_component"
|
4
5
|
|
5
6
|
module SHACL
|
6
7
|
# Based on the SPARQL Algebra, operators for executing a patch
|
7
8
|
module Algebra
|
8
|
-
autoload :
|
9
|
-
autoload :
|
10
|
-
autoload :
|
11
|
-
autoload :
|
12
|
-
autoload :
|
13
|
-
autoload :
|
14
|
-
autoload :
|
15
|
-
autoload :
|
16
|
-
autoload :
|
17
|
-
autoload :
|
9
|
+
autoload :AndConstraintComponent, 'shacl/algebra/and.rb'
|
10
|
+
autoload :NodeShape, 'shacl/algebra/node_shape.rb'
|
11
|
+
autoload :NotConstraintComponent, 'shacl/algebra/not.rb'
|
12
|
+
autoload :OrConstraintComponent, 'shacl/algebra/or.rb'
|
13
|
+
autoload :PatternConstraintComponent, 'shacl/algebra/pattern.rb'
|
14
|
+
autoload :PropertyShape, 'shacl/algebra/property_shape.rb'
|
15
|
+
autoload :QualifiedMaxCountConstraintComponent, 'shacl/algebra/qualified_value.rb'
|
16
|
+
autoload :QualifiedMinCountConstraintComponent, 'shacl/algebra/qualified_value.rb'
|
17
|
+
autoload :QualifiedValueConstraintComponent, 'shacl/algebra/qualified_value.rb'
|
18
|
+
autoload :Shape, 'shacl/algebra/shape.rb'
|
19
|
+
autoload :SPARQLConstraintComponent, 'shacl/algebra/sparql_constraint.rb'
|
20
|
+
autoload :XoneConstraintComponent, 'shacl/algebra/xone.rb'
|
18
21
|
|
19
22
|
def self.from_json(operator, **options)
|
20
|
-
raise
|
23
|
+
raise SHACL::Error, "from_json: operator not a Hash: #{operator.inspect}" unless operator.is_a?(Hash)
|
24
|
+
|
25
|
+
# If operator is a hash containing @list, it is a single array value.
|
26
|
+
# Note: context does not use @container: @list on this terms to preserve cardinality expectations
|
27
|
+
return operator['@list'].map {|e| from_json(e, **options)} if operator.key?('@list')
|
28
|
+
|
21
29
|
type = operator.fetch('type', [])
|
22
|
-
|
30
|
+
if type.empty?
|
31
|
+
type << if operator["path"]
|
32
|
+
'PropertyShape'
|
33
|
+
elsif operator['nodeValidator'] || operator['propertyValidator'] || operator['validator']
|
34
|
+
'ConstraintComponent'
|
35
|
+
else
|
36
|
+
'NodeShape'
|
37
|
+
end
|
38
|
+
end
|
23
39
|
klass = case
|
24
40
|
when type.include?('NodeShape') then NodeShape
|
25
41
|
when type.include?('PropertyShape') then PropertyShape
|
26
|
-
|
42
|
+
when type.include?('ConstraintComponent') then ConstraintComponent
|
43
|
+
else raise SHACL::Error, "from_json: unknown type #{type.inspect}"
|
27
44
|
end
|
28
45
|
|
29
46
|
klass.from_json(operator, **options)
|
data/lib/shacl/context.rb
CHANGED
@@ -17,20 +17,25 @@ class JSON::LD::Context
|
|
17
17
|
"equals" => TermDefinition.new("equals", id: "http://www.w3.org/ns/shacl#equals", type_mapping: "@id"),
|
18
18
|
"id" => TermDefinition.new("id", id: "@id", simple: true),
|
19
19
|
"ignoredProperties" => TermDefinition.new("ignoredProperties", id: "http://www.w3.org/ns/shacl#ignoredProperties", type_mapping: "@id", container_mapping: "@list"),
|
20
|
+
"imports" => TermDefinition.new("imports", id: "http://www.w3.org/2002/07/owl#imports", type_mapping: "@id"),
|
20
21
|
"in" => TermDefinition.new("in", id: "http://www.w3.org/ns/shacl#in", type_mapping: "@none", container_mapping: "@list"),
|
21
22
|
"inversePath" => TermDefinition.new("inversePath", id: "http://www.w3.org/ns/shacl#inversePath", type_mapping: "@id"),
|
22
23
|
"label" => TermDefinition.new("label", id: "http://www.w3.org/2000/01/rdf-schema#label", simple: true),
|
23
24
|
"languageIn" => TermDefinition.new("languageIn", id: "http://www.w3.org/ns/shacl#languageIn", container_mapping: "@list"),
|
24
25
|
"lessThan" => TermDefinition.new("lessThan", id: "http://www.w3.org/ns/shacl#lessThan", type_mapping: "@id"),
|
25
26
|
"lessThanOrEquals" => TermDefinition.new("lessThanOrEquals", id: "http://www.w3.org/ns/shacl#lessThanOrEquals", type_mapping: "@id"),
|
27
|
+
"namespace" => TermDefinition.new("namespace", id: "http://www.w3.org/ns/shacl#namespace", type_mapping: "http://www.w3.org/2001/XMLSchema#anyURI"),
|
26
28
|
"nodeKind" => TermDefinition.new("nodeKind", id: "http://www.w3.org/ns/shacl#nodeKind", type_mapping: "@vocab"),
|
27
29
|
"or" => TermDefinition.new("or", id: "http://www.w3.org/ns/shacl#or", type_mapping: "@id", container_mapping: "@list"),
|
30
|
+
"owl" => TermDefinition.new("owl", id: "http://www.w3.org/2002/07/owl#", simple: true, prefix: true),
|
28
31
|
"path" => TermDefinition.new("path", id: "http://www.w3.org/ns/shacl#path", type_mapping: "@none"),
|
32
|
+
"prefixes" => TermDefinition.new("prefixes", id: "http://www.w3.org/ns/shacl#prefixes", type_mapping: "@id"),
|
29
33
|
"property" => TermDefinition.new("property", id: "http://www.w3.org/ns/shacl#property", type_mapping: "@id"),
|
30
34
|
"rdfs" => TermDefinition.new("rdfs", id: "http://www.w3.org/2000/01/rdf-schema#", simple: true, prefix: true),
|
31
35
|
"severity" => TermDefinition.new("severity", id: "http://www.w3.org/ns/shacl#severity", type_mapping: "@vocab"),
|
32
36
|
"sh" => TermDefinition.new("sh", id: "http://www.w3.org/ns/shacl#", simple: true, prefix: true),
|
33
37
|
"shacl" => TermDefinition.new("shacl", id: "http://www.w3.org/ns/shacl#", simple: true, prefix: true),
|
38
|
+
"sparql" => TermDefinition.new("sparql", id: "http://www.w3.org/ns/shacl#sparql", type_mapping: "@id"),
|
34
39
|
"targetClass" => TermDefinition.new("targetClass", id: "http://www.w3.org/ns/shacl#targetClass", type_mapping: "@id"),
|
35
40
|
"targetNode" => TermDefinition.new("targetNode", id: "http://www.w3.org/ns/shacl#targetNode", type_mapping: "@none"),
|
36
41
|
"type" => TermDefinition.new("type", id: "@type", container_mapping: "@set"),
|