shacl 0.1.1 → 0.3.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 +19 -14
- data/VERSION +1 -1
- data/etc/doap.ttl +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 +46 -13
- data/lib/shacl/validation_report.rb +6 -2
- data/lib/shacl/validation_result.rb +28 -21
- data/lib/shacl.rb +10 -5
- metadata +51 -29
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
|