shacl 0.2.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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"),
|