shacl 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  module SHACL::Algebra
2
2
  ##
3
- class And < Operator
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.merge(severity: RDF::Vocab::SHACL.Violation)
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], depth: depth + 1, **options) if self.respond_to?("builtin_#{k}".to_sym)
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.fetch(:severity),
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.fetch(:severity),
63
+ resultSeverity: options[:severity],
70
64
  component: RDF::Vocab::SHACL.NodeConstraintComponent,
71
65
  **options)
72
66
  else
@@ -1,12 +1,13 @@
1
1
  module SHACL::Algebra
2
2
  ##
3
- class Not < Operator
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
- ALL_KEYS = %i(
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
- pattern flags languageIn uniqueLang
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 validaed
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
- next if v.nil?
207
+ k = k.to_sym
208
+ next if v.nil? || PARAMETERS.include?(k)
51
209
  case k
52
210
  # List properties
53
- when 'and'
54
- elements = as_array(v).map {|vv| SHACL::Algebra.from_json(vv, **options)}
55
- operands << And.new(*elements, **options.dup)
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 'qualifiedValueShape'
77
- elements = as_array(v).map {|vv| SHACL::Algebra.from_json(vv, **options)}
78
- operands << QualifiedValueShape.new(*elements, **options.dup)
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 if v
85
- when 'targetObjectsOf' then node_opts[:targetObjectsOf] = as_array(v).map {|vv| iri(vv, **options)} if v
86
- when 'targetSubjectsOf' then node_opts[:targetSubjectsOf] = as_array(v).map {|vv| iri(vv, **options)} if v
87
- when 'type' then node_opts[:type] = as_array(v).map {|vv| iri(vv, **options)} if v
88
- when 'xone'
89
- elements = as_array(v).map {|vv| SHACL::Algebra.from_json(vv, **options)}
90
- operands << Xone.new(*elements, **options.dup)
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
- # Add as a plain option if it is recognized
93
- node_opts[k.to_sym] = to_rdf(k.to_sym, v, **options) if ALL_KEYS.include?(k.to_sym)
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 "patH" attribute into a SPARQL Property Path and evaluate to find related nodes.
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 = ALL_KEYS.inject([self.class.const_get(:NAME)]) do |memo, sym|
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
 
@@ -1,6 +1,6 @@
1
1
  module SHACL::Algebra
2
2
  ##
3
- class Or < Operator
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.merge(severity: RDF::Vocab::SHACL.Violation)
20
-
21
- # Add some instance options to the argument
22
- options = %i{
23
- flags
24
- qualifiedMinCount
25
- qualifiedMaxCount
26
- qualifiedValueShapesDisjoint
27
- severity
28
- }.inject(options) do |memo, sym|
29
- @options[sym] ? memo.merge(sym => @options[sym]) : memo
30
- end
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?(QualifiedValueShape)
47
+ if op.is_a?(QualifiedValueConstraintComponent) || op.is_a?(SPARQLConstraintComponent)
58
48
  # All value nodes are passed
59
- op.conforms(node, value_nodes: value_nodes, path: path, depth: depth + 1, **options)
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 QualifiedValueShape < Operator
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
- max_count = options.fetch(:qualifiedMaxCount, 0).to_i
15
- min_count = options.fetch(:qualifiedMinCount, 0).to_i
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 = options[:qualifiedValueShapesDisjoint]
23
+ disjoint = !!params[:qualifiedValueShapesDisjoint]
18
24
 
19
- operands.map do |op|
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