shacl 0.2.1 → 0.3.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 +7 -2
- 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 +9 -0
- data/lib/shacl/algebra/node_shape.rb +10 -16
- data/lib/shacl/algebra/not.rb +3 -2
- data/lib/shacl/algebra/operator.rb +276 -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} +19 -5
- 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 +20 -12
- data/lib/shacl/context.rb +5 -0
- data/lib/shacl/shapes.rb +40 -11
- data/lib/shacl/validation_result.rb +22 -19
- data/lib/shacl.rb +10 -5
- metadata +32 -3
data/lib/shacl/algebra/and.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module SHACL::Algebra
|
2
2
|
##
|
3
|
-
class
|
3
|
+
class AndConstraintComponent < ConstraintComponent
|
4
4
|
NAME = :and
|
5
5
|
|
6
6
|
##
|
@@ -25,7 +25,8 @@ module SHACL::Algebra
|
|
25
25
|
# ]
|
26
26
|
# ) .
|
27
27
|
#
|
28
|
-
# @param [RDF::Term] node
|
28
|
+
# @param [RDF::Term] node focus node
|
29
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
29
30
|
# @param [Hash{Symbol => Object}] options
|
30
31
|
# @return [Array<SHACL::ValidationResult>]
|
31
32
|
def conforms(node, path: nil, depth: 0, **options)
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require_relative "operator"
|
2
|
+
|
3
|
+
module SHACL::Algebra
|
4
|
+
##
|
5
|
+
# Constraint Components define basic constraint behaivor through _mandatory_ and _optional_ parameters. Constraints are accessed through their parameters.
|
6
|
+
#
|
7
|
+
class ConstraintComponent < Operator
|
8
|
+
end
|
9
|
+
end
|
@@ -9,30 +9,22 @@ module SHACL::Algebra
|
|
9
9
|
#
|
10
10
|
# A node conforms if it is not deactivated and all of its operands 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 one or more validation results for each operand.
|
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
|
19
|
+
options[:severity] = @options[:severity] if @options[:severity]
|
20
|
+
options[:severity] ||= RDF::Vocab::SHACL.Violation
|
20
21
|
log_debug(NAME, depth: depth) {SXP::Generator.string({id: id, node: node}.to_sxp_bin)}
|
21
22
|
|
22
|
-
# Add some instance options to the argument
|
23
|
-
options = %i{
|
24
|
-
flags
|
25
|
-
qualifiedMinCount
|
26
|
-
qualifiedMaxCount
|
27
|
-
qualifiedValueShapesDisjoint
|
28
|
-
severity
|
29
|
-
}.inject(options) do |memo, sym|
|
30
|
-
@options[sym] ? memo.merge(sym => @options[sym]) : memo
|
31
|
-
end
|
32
|
-
|
33
23
|
# Evaluate against builtins
|
34
24
|
builtin_results = @options.map do |k, v|
|
35
|
-
self.send("builtin_#{k}".to_sym, v, node, nil, [node],
|
25
|
+
self.send("builtin_#{k}".to_sym, v, node, nil, [node],
|
26
|
+
depth: depth + 1,
|
27
|
+
**options) if self.respond_to?("builtin_#{k}".to_sym)
|
36
28
|
end.flatten.compact
|
37
29
|
|
38
30
|
# Handle closed shapes
|
@@ -49,10 +41,12 @@ module SHACL::Algebra
|
|
49
41
|
value: statement.object,
|
50
42
|
path: statement.predicate,
|
51
43
|
message: "closed node has extra property",
|
52
|
-
resultSeverity: options
|
44
|
+
resultSeverity: options[:severity],
|
53
45
|
component: RDF::Vocab::SHACL.ClosedConstraintComponent,
|
54
46
|
**options)
|
55
47
|
end.compact
|
48
|
+
elsif @options[:ignoredProperties]
|
49
|
+
raise SHACL::Error, "shape has ignoredProperties without being closed"
|
56
50
|
end
|
57
51
|
|
58
52
|
# Evaluate against operands
|
@@ -66,7 +60,7 @@ module SHACL::Algebra
|
|
66
60
|
not_satisfied(focus: node,
|
67
61
|
value: node,
|
68
62
|
message: "node does not conform to #{op.id}",
|
69
|
-
resultSeverity: options
|
63
|
+
resultSeverity: options[:severity],
|
70
64
|
component: RDF::Vocab::SHACL.NodeConstraintComponent,
|
71
65
|
**options)
|
72
66
|
else
|
data/lib/shacl/algebra/not.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
module SHACL::Algebra
|
2
2
|
##
|
3
|
-
class
|
3
|
+
class NotConstraintComponent < ConstraintComponent
|
4
4
|
NAME = :not
|
5
5
|
|
6
6
|
##
|
7
7
|
# Specifies the condition that each value node cannot conform to a given shape. This is comparable to negation and the logical "not" operator.
|
8
8
|
#
|
9
|
-
# @param [RDF::Term] node
|
9
|
+
# @param [RDF::Term] node focus node
|
10
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
10
11
|
# @param [Hash{Symbol => Object}] options
|
11
12
|
# @return [Array<SHACL::ValidationResult>]
|
12
13
|
def conforms(node, path: nil, depth: 0, **options)
|
@@ -15,26 +15,179 @@ module SHACL::Algebra
|
|
15
15
|
# All keys associated with shapes which are set in options
|
16
16
|
#
|
17
17
|
# @return [Array<Symbol>]
|
18
|
-
|
18
|
+
BUILTIN_KEYS = %i(
|
19
19
|
id type label name comment description deactivated severity
|
20
|
+
message
|
20
21
|
order group defaultValue path
|
21
22
|
targetNode targetClass targetSubjectsOf targetObjectsOf
|
22
23
|
class datatype nodeKind
|
23
24
|
minCount maxCount
|
24
25
|
minExclusive minInclusive maxExclusive maxInclusive
|
25
26
|
minLength maxLength
|
26
|
-
|
27
|
-
qualifiedValueShapesDisjoint qualifiedMinCount qualifiedMaxCount
|
27
|
+
languageIn uniqueLang
|
28
28
|
equals disjoint lessThan lessThanOrEquals
|
29
29
|
closed ignoredProperties hasValue in
|
30
|
+
declare namespace prefix
|
30
31
|
).freeze
|
31
32
|
|
32
33
|
# Initialization options
|
33
34
|
attr_accessor :options
|
34
35
|
|
35
|
-
# Graph against which shapes are
|
36
|
+
# Graph against which shapes are validated.
|
37
|
+
# @return [RDF::Queryable]
|
36
38
|
attr_accessor :graph
|
37
39
|
|
40
|
+
# Graph from which original shapes were loaded.
|
41
|
+
# @return [RDF::Graph]
|
42
|
+
attr_accessor :shapes_graph
|
43
|
+
# Parameters to components.
|
44
|
+
PARAMETERS = {
|
45
|
+
and: {class: :AndConstraintComponent},
|
46
|
+
class: {
|
47
|
+
class: :ClassConstraintComponent,
|
48
|
+
nodeKind: :IRI,
|
49
|
+
},
|
50
|
+
closed: {
|
51
|
+
class: :ClosedConstraintComponent,
|
52
|
+
datatype: RDF::XSD.boolean,
|
53
|
+
},
|
54
|
+
datatype: {
|
55
|
+
class: :DatatypeConstraintComponent,
|
56
|
+
nodeKind: :IRI,
|
57
|
+
maxCount: 1,
|
58
|
+
},
|
59
|
+
disjoint: {
|
60
|
+
class: :DisjointConstraintComponent,
|
61
|
+
nodeKind: :IRI,
|
62
|
+
},
|
63
|
+
equals: {
|
64
|
+
class: :EqualsConstraintComponent,
|
65
|
+
nodeKind: :IRI,
|
66
|
+
},
|
67
|
+
expression: {class: :ExpressionConstraintComponent},
|
68
|
+
flags: {
|
69
|
+
class: :PatternConstraintComponent,
|
70
|
+
datatype: RDF::XSD.string,
|
71
|
+
optional: true
|
72
|
+
},
|
73
|
+
hasValue: {
|
74
|
+
class: :HasValueConstraintComponent,
|
75
|
+
nodeKind: :IRIOrLiteral,
|
76
|
+
},
|
77
|
+
ignoredProperties: {
|
78
|
+
class: :ClosedConstraintComponent,
|
79
|
+
nodeKind: :IRI, # Added
|
80
|
+
optional: true,
|
81
|
+
},
|
82
|
+
in: {
|
83
|
+
class: :InConstraintComponent,
|
84
|
+
nodeKind: :IRIOrLiteral,
|
85
|
+
#maxCount: 1, # List internalized
|
86
|
+
},
|
87
|
+
languageIn: {
|
88
|
+
class: :LanguageInConstraintComponent,
|
89
|
+
datatype: RDF::XSD.string, # Added
|
90
|
+
#maxCount: 1, # List internalized
|
91
|
+
},
|
92
|
+
lessThan: {
|
93
|
+
class: :LessThanConstraintComponent,
|
94
|
+
nodeKind: :IRI,
|
95
|
+
},
|
96
|
+
lessThanOrEquals: {
|
97
|
+
class: :LessThanOrEqualsConstraintComponent,
|
98
|
+
nodeKind: :IRI,
|
99
|
+
},
|
100
|
+
maxCount: {
|
101
|
+
class: :MaxCountConstraintComponent,
|
102
|
+
datatype: RDF::XSD.integer,
|
103
|
+
maxCount: 1,
|
104
|
+
},
|
105
|
+
maxExclusive: {
|
106
|
+
class: :MaxExclusiveConstraintComponent,
|
107
|
+
maxCount: 1,
|
108
|
+
nodeKind: :Literal,
|
109
|
+
},
|
110
|
+
maxInclusive: {
|
111
|
+
class: :MaxInclusiveConstraintComponent,
|
112
|
+
maxCount: 1,
|
113
|
+
nodeKind: :Literal,
|
114
|
+
},
|
115
|
+
maxLength: {
|
116
|
+
class: :MaxLengthConstraintComponent,
|
117
|
+
datatype: RDF::XSD.integer,
|
118
|
+
maxCount: 1,
|
119
|
+
},
|
120
|
+
minCount: {
|
121
|
+
class: :MinCountConstraintComponent,
|
122
|
+
datatype: RDF::XSD.integer,
|
123
|
+
maxCount: 1,
|
124
|
+
},
|
125
|
+
minExclusive: {
|
126
|
+
class: :MinExclusiveConstraintComponent,
|
127
|
+
maxCount: 1,
|
128
|
+
nodeKind: :Literal,
|
129
|
+
},
|
130
|
+
minInclusive: {
|
131
|
+
class: :MinInclusiveConstraintComponent,
|
132
|
+
maxCount: 1,
|
133
|
+
nodeKind: :Literal,
|
134
|
+
},
|
135
|
+
minLength: {
|
136
|
+
class: :MinLengthConstraintComponent,
|
137
|
+
datatype: RDF::XSD.integer,
|
138
|
+
maxCount: 1,
|
139
|
+
},
|
140
|
+
node: {class: :NodeConstraintComponent},
|
141
|
+
nodeKind: {
|
142
|
+
class: :NodeKindConstraintComponent,
|
143
|
+
in: %i(BlankNode IRI Literal BlankNodeOrIRI BlankNodeOrLiteral IRIOrLiteral),
|
144
|
+
maxCount: 1,
|
145
|
+
},
|
146
|
+
not: {class: :NotConstraintComponent},
|
147
|
+
or: {class: :OrConstraintComponent},
|
148
|
+
pattern: {
|
149
|
+
class: :PatternConstraintComponent,
|
150
|
+
datatype: RDF::XSD.string,
|
151
|
+
},
|
152
|
+
property: {class: :PropertyConstraintComponent},
|
153
|
+
qualifiedMaxCount: {
|
154
|
+
class: :QualifiedMaxCountConstraintComponent,
|
155
|
+
datatype: RDF::XSD.integer,
|
156
|
+
},
|
157
|
+
qualifiedValueShape: {
|
158
|
+
class: %i(QualifiedMaxCountConstraintComponent QualifiedMinCountConstraintComponent),
|
159
|
+
},
|
160
|
+
qualifiedValueShapesDisjoint: {
|
161
|
+
class: %i(QualifiedMaxCountConstraintComponent QualifiedMinCountConstraintComponent),
|
162
|
+
datatype: RDF::XSD.boolean,
|
163
|
+
optional: true,
|
164
|
+
},
|
165
|
+
qualifiedMinCount: {
|
166
|
+
class: :QualifiedMinCountConstraintComponent,
|
167
|
+
datatype: RDF::XSD.integer
|
168
|
+
},
|
169
|
+
sparql: {class: :SPARQLConstraintComponent},
|
170
|
+
uniqueLang: {
|
171
|
+
class: :UniqueLangConstraintComponent,
|
172
|
+
datatype: RDF::XSD.boolean,
|
173
|
+
maxCount: 1,
|
174
|
+
},
|
175
|
+
xone: {class: :XoneConstraintComponent},
|
176
|
+
}
|
177
|
+
|
178
|
+
# Constraint Component classes indexed to their mandatory and optional parameters.
|
179
|
+
#
|
180
|
+
# @note for builtins, corresponding Ruby classes may not exist.
|
181
|
+
COMPONENT_PARAMS = PARAMETERS.inject({}) do |memo, (param, properties)|
|
182
|
+
memo.merge(Array(properties[:class]).inject(memo) do |mem, cls|
|
183
|
+
entry = mem.fetch(cls, {})
|
184
|
+
param_type = properties[:optional] ? :optional : :mandatory
|
185
|
+
entry[param_type] ||= []
|
186
|
+
entry[param_type] << param
|
187
|
+
mem.merge(cls => entry)
|
188
|
+
end)
|
189
|
+
end
|
190
|
+
|
38
191
|
## Class methods
|
39
192
|
class << self
|
40
193
|
##
|
@@ -45,52 +198,130 @@ module SHACL::Algebra
|
|
45
198
|
# @return [Operator]
|
46
199
|
def from_json(operator, **options)
|
47
200
|
operands = []
|
201
|
+
|
202
|
+
# Node options used to instantiate the relevant class instance.
|
48
203
|
node_opts = options.dup
|
204
|
+
|
205
|
+
# Node Options and operands on shape or node, which are not Constraint Component Parameters
|
49
206
|
operator.each do |k, v|
|
50
|
-
|
207
|
+
k = k.to_sym
|
208
|
+
next if v.nil? || PARAMETERS.include?(k)
|
51
209
|
case k
|
52
210
|
# List properties
|
53
|
-
when
|
54
|
-
|
55
|
-
|
56
|
-
when 'class' then node_opts[:class] = as_array(v).map {|vv| iri(vv, **options)} if v
|
57
|
-
when 'datatype' then node_opts[:datatype] = iri(v, **options)
|
58
|
-
when 'disjoint' then node_opts[:disjoint] = as_array(v).map {|vv| iri(vv, **options)} if v
|
59
|
-
when 'equals' then node_opts[:equals] = iri(v, **options)
|
60
|
-
when 'id' then node_opts[:id] = iri(v, vocab: false, **options)
|
61
|
-
when 'ignoredProperties' then node_opts[:ignoredProperties] = as_array(v).map {|vv| iri(vv, **options)} if v
|
62
|
-
when 'lessThan' then node_opts[:lessThan] = iri(v, **options)
|
63
|
-
when 'lessThanOrEquals' then node_opts[:lessThanOrEquals] = iri(v, **options)
|
64
|
-
when 'node'
|
65
|
-
operands.push(*as_array(v).map {|vv| NodeShape.from_json(vv, **options)})
|
66
|
-
when 'nodeKind' then node_opts[:nodeKind] = iri(v, **options)
|
67
|
-
when 'not'
|
68
|
-
elements = as_array(v).map {|vv| SHACL::Algebra.from_json(vv, **options)}
|
69
|
-
operands << Not.new(*elements, **options.dup)
|
70
|
-
when 'or'
|
71
|
-
elements = as_array(v).map {|vv| SHACL::Algebra.from_json(vv, **options)}
|
72
|
-
operands << Or.new(*elements, **options.dup)
|
73
|
-
when 'path' then node_opts[:path] = parse_path(v, **options)
|
74
|
-
when 'property'
|
211
|
+
when :id then node_opts[:id] = iri(v, vocab: false, **options)
|
212
|
+
when :path then node_opts[:path] = parse_path(v, **options)
|
213
|
+
when :property
|
75
214
|
operands.push(*as_array(v).map {|vv| PropertyShape.from_json(vv, **options)})
|
76
|
-
when
|
77
|
-
|
78
|
-
|
79
|
-
when 'severity' then node_opts[:severity] = iri(v, **options)
|
80
|
-
when 'targetClass' then node_opts[:targetClass] = as_array(v).map {|vv| iri(vv, **options)} if v
|
81
|
-
when 'targetNode'
|
215
|
+
when :severity then node_opts[:severity] = iri(v, **options)
|
216
|
+
when :targetClass then node_opts[:targetClass] = as_array(v).map {|vv| iri(vv, **options)}
|
217
|
+
when :targetNode
|
82
218
|
node_opts[:targetNode] = as_array(v).map do |vv|
|
83
219
|
from_expanded_value(vv, **options)
|
84
|
-
end
|
85
|
-
when
|
86
|
-
when
|
87
|
-
when
|
88
|
-
|
89
|
-
|
90
|
-
|
220
|
+
end
|
221
|
+
when :targetObjectsOf then node_opts[:targetObjectsOf] = as_array(v).map {|vv| iri(vv, **options)}
|
222
|
+
when :targetSubjectsOf then node_opts[:targetSubjectsOf] = as_array(v).map {|vv| iri(vv, **options)}
|
223
|
+
when :type then node_opts[:type] = as_array(v).map {|vv| iri(vv, **options)}
|
224
|
+
else
|
225
|
+
if BUILTIN_KEYS.include?(k)
|
226
|
+
# Add as a plain option otherwise
|
227
|
+
node_opts[k] = to_rdf(k, v, **options)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Node Options and operands on shape or node, which are Constraint Component Parameters.
|
233
|
+
# For constraints with a defined Ruby class, the primary parameter is the NAME from the constraint class. Other parameters are added as named operands to the component operator.
|
234
|
+
used_components = {}
|
235
|
+
operator.each do |k, v|
|
236
|
+
k = k.to_sym
|
237
|
+
next if v.nil? || !PARAMETERS.include?(k)
|
238
|
+
param_props = PARAMETERS[k]
|
239
|
+
param_classes = Array(param_props[:class])
|
240
|
+
|
241
|
+
# Keep track of components which have been used.
|
242
|
+
param_classes.each {|cls| used_components[cls] ||= {}}
|
243
|
+
|
244
|
+
# Check parameter constraints
|
245
|
+
v = as_array(v)
|
246
|
+
if param_props[:maxCount] && v.length > param_props[:maxCount]
|
247
|
+
raise SHACL::Error, "Property #{k} on #{self.const_get(:NAME)} has too many values: #{v.inspect}"
|
248
|
+
end
|
249
|
+
|
250
|
+
# If an optional parameter exists without corresponding mandatory parameters on a given shape, raise a SHACL::Error.
|
251
|
+
#
|
252
|
+
# Records any instances of components which are created to re-attach non-primary parameters after all operators are processed.
|
253
|
+
instances = case k
|
254
|
+
# List properties
|
255
|
+
when :node
|
256
|
+
as_array(v).map {|vv| NodeShape.from_json(vv, **options)}
|
257
|
+
when :property
|
258
|
+
as_array(v).map {|vv| PropertyShape.from_json(vv, **options)}
|
259
|
+
when :sparql
|
260
|
+
as_array(v).map {|vv| SPARQLConstraintComponent.from_json(vv, **options)}
|
91
261
|
else
|
92
|
-
#
|
93
|
-
|
262
|
+
# Process parameter values based on nodeKind, in, and datatype.
|
263
|
+
elements = if param_props[:nodeKind]
|
264
|
+
case param_props[:nodeKind]
|
265
|
+
when :IRI
|
266
|
+
v.map {|vv| iri(vv, **options)}
|
267
|
+
when :Literal
|
268
|
+
v.map do |vv|
|
269
|
+
vv.is_a?(Hash) ?
|
270
|
+
from_expanded_value(vv, **options) :
|
271
|
+
RDF::Literal(vv)
|
272
|
+
end
|
273
|
+
when :IRIOrLiteral
|
274
|
+
to_rdf(k, v, **options)
|
275
|
+
end
|
276
|
+
elsif param_props[:in]
|
277
|
+
v.map do |vv|
|
278
|
+
iri(vv, **options) if param_props[:in].include?(vv.to_sym)
|
279
|
+
end
|
280
|
+
elsif param_props[:datatype]
|
281
|
+
v.map {|vv| RDF::Literal(vv, datatype: param_props[:datatype])}
|
282
|
+
else
|
283
|
+
v.map {|vv| SHACL::Algebra.from_json(vv, **options)}
|
284
|
+
end
|
285
|
+
|
286
|
+
# Builtins are added as options to the operator, otherwise, they are class instances of constraint components added as operators.
|
287
|
+
if BUILTIN_KEYS.include?(k)
|
288
|
+
node_opts[k] = elements
|
289
|
+
[] # No instances created
|
290
|
+
else
|
291
|
+
klass = SHACL::Algebra.const_get(Array(param_props[:class]).first)
|
292
|
+
|
293
|
+
name = klass.const_get(:NAME)
|
294
|
+
# If the key `k` is the same as the NAME of the class, create the instance with the defined element values.
|
295
|
+
if name == k
|
296
|
+
elements.map {|e| klass.new(*e, **options.dup)}
|
297
|
+
else
|
298
|
+
# Add non-primary parameters for subsequent insertion
|
299
|
+
param_classes.each do |cls|
|
300
|
+
(used_components[cls][:parameters] ||= []) << elements.unshift(k)
|
301
|
+
end
|
302
|
+
[] # No instances created
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Record the instances created by class and add as operands
|
308
|
+
param_classes.each do |cls|
|
309
|
+
used_components[cls][:instances] = instances
|
310
|
+
end
|
311
|
+
operands.push(*instances)
|
312
|
+
end
|
313
|
+
|
314
|
+
# Append any parameters to the used components
|
315
|
+
used_components.each do |cls, props|
|
316
|
+
instances = props[:instances]
|
317
|
+
next unless instances # BUILTINs
|
318
|
+
|
319
|
+
parameters = props.fetch(:parameters, [])
|
320
|
+
instances.each do |op|
|
321
|
+
parameters.each do |param|
|
322
|
+
# Note the potential that the parameter gets added twice, if there are multiple classes for both the primary and secondary paramters.
|
323
|
+
op.operands << param
|
324
|
+
end
|
94
325
|
end
|
95
326
|
end
|
96
327
|
|
@@ -189,7 +420,7 @@ module SHACL::Algebra
|
|
189
420
|
end
|
190
421
|
|
191
422
|
##
|
192
|
-
# Parse the "
|
423
|
+
# Parse the "path" attribute into a SPARQL Property Path and evaluate to find related nodes.
|
193
424
|
#
|
194
425
|
# @param [Object] path
|
195
426
|
# @return [RDF::URI, SPARQL::Algebra::Expression]
|
@@ -282,8 +513,9 @@ module SHACL::Algebra
|
|
282
513
|
raise NotImplemented
|
283
514
|
end
|
284
515
|
|
516
|
+
# Create structure for serializing this component/shape, beginning with its cononical name.
|
285
517
|
def to_sxp_bin
|
286
|
-
expressions =
|
518
|
+
expressions = BUILTIN_KEYS.inject([self.class.const_get(:NAME)]) do |memo, sym|
|
287
519
|
@options[sym] ? memo.push([sym, *@options[sym]]) : memo
|
288
520
|
end + operands
|
289
521
|
|
data/lib/shacl/algebra/or.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module SHACL::Algebra
|
2
2
|
##
|
3
|
-
class
|
3
|
+
class OrConstraintComponent < ConstraintComponent
|
4
4
|
NAME = :or
|
5
5
|
|
6
6
|
##
|
@@ -21,7 +21,8 @@ module SHACL::Algebra
|
|
21
21
|
# ]
|
22
22
|
# ) .
|
23
23
|
#
|
24
|
-
# @param [RDF::Term] node
|
24
|
+
# @param [RDF::Term] node focus node
|
25
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
25
26
|
# @param [Hash{Symbol => Object}] options
|
26
27
|
# @return [Array<SHACL::ValidationResult>]
|
27
28
|
def conforms(node, path: nil, depth: 0, **options)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module SHACL::Algebra
|
2
|
+
##
|
3
|
+
class PatternConstraintComponent < ConstraintComponent
|
4
|
+
NAME = :pattern
|
5
|
+
|
6
|
+
##
|
7
|
+
# Specifies a regular expression that each value node matches to satisfy the condition.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# ex:PatternExampleShape
|
11
|
+
# a sh:NodeShape ;
|
12
|
+
# sh:targetNode ex:Bob, ex:Alice, ex:Carol ;
|
13
|
+
# sh:property [
|
14
|
+
# sh:path ex:bCode ;
|
15
|
+
# sh:pattern "^B" ; # starts with 'B'
|
16
|
+
# sh:flags "i" ; # Ignore case
|
17
|
+
# ] .
|
18
|
+
#
|
19
|
+
# @param [RDF::Term] node focus node
|
20
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
21
|
+
# @param [Hash{Symbol => Object}] options
|
22
|
+
# @return [Array<SHACL::ValidationResult>]
|
23
|
+
def conforms(node, path: nil, depth: 0, **options)
|
24
|
+
log_debug(NAME, depth: depth) {SXP::Generator.string({node: node}.to_sxp_bin)}
|
25
|
+
pattern = Array(operands.first).first
|
26
|
+
flags = operands.last.last if operands.last.is_a?(Array) && operands.last.first == :flags
|
27
|
+
flags = flags.to_s
|
28
|
+
regex_opts = 0
|
29
|
+
regex_opts |= Regexp::MULTILINE if flags.include?(?m)
|
30
|
+
regex_opts |= Regexp::IGNORECASE if flags.include?(?i)
|
31
|
+
regex_opts |= Regexp::EXTENDED if flags.include?(?x)
|
32
|
+
pat = Regexp.new(pattern, regex_opts)
|
33
|
+
|
34
|
+
compares = !node.node? && pat.match?(node.to_s)
|
35
|
+
satisfy(focus: node, path: path,
|
36
|
+
value: node,
|
37
|
+
message: "is#{' not' unless compares} a match #{pat.inspect}",
|
38
|
+
resultSeverity: (options.fetch(:severity) unless compares),
|
39
|
+
component: RDF::Vocab::SHACL.PatternConstraintComponent,
|
40
|
+
**options)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -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,22 +1,28 @@
|
|
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].to_i
|
21
|
+
min_count = params[:qualifiedMinCount].to_i
|
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
|
@@ -44,4 +50,12 @@ module SHACL::Algebra
|
|
44
50
|
end
|
45
51
|
end
|
46
52
|
end
|
53
|
+
|
54
|
+
# Version on QualifiedConstraintComponent with required `qualifiedMaxCount` parameter
|
55
|
+
class QualifiedMaxCountConstraintComponent < QualifiedValueConstraintComponent
|
56
|
+
end
|
57
|
+
|
58
|
+
# Version on QualifiedConstraintComponent with required `qualifiedMinCount` parameter
|
59
|
+
class QualifiedMinCountConstraintComponent < QualifiedValueConstraintComponent
|
60
|
+
end
|
47
61
|
end
|