shacl 0.2.1 → 0.4.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,67 @@
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
+
9
+ # Class Methods
10
+ class << self
11
+ ##
12
+ # Creates an operator instance from a parsed SHACL representation.
13
+ #
14
+ # Special case for SPARQL ConstraintComponents.
15
+ #
16
+ # @param [Hash] operator
17
+ # @param [Hash] options ({})
18
+ # @option options [Hash{String => RDF::URI}] :prefixes
19
+ # @return [Operator]
20
+ def from_json(operator, **options)
21
+ operands = []
22
+
23
+ # Component is known by its subject IRI
24
+ id = operator.fetch('id')
25
+
26
+ # Component class (for instantiation) is based on the _local name_ of the component IRI
27
+ class_name = ncname(id)
28
+
29
+ parameters = operator.fetch('parameter', []).inject({}) do |memo, param|
30
+ # Symbolize keys
31
+ param = param.inject({}) {|memo, (k,v)| memo.merge(k.to_sym => v)}
32
+
33
+ plc = ncname(param[:path])
34
+
35
+ # Add class and local name
36
+ param = param.merge(class: class_name, local_name: plc)
37
+ memo.merge(param[:path] => param)
38
+ end
39
+
40
+ # Add parameters to operator lookup
41
+ add_component(class_name, parameters)
42
+
43
+ # Add parameter identifiers to operands
44
+ operands << [:parameters, parameters.keys]
45
+
46
+ # FIXME: labelTemplate
47
+
48
+ validator = %w(validator nodeValidator propertyValidator).inject(nil) do |memo, p|
49
+ memo || (SPARQLConstraintComponent.from_json(operator[p]) if operator.key?(p))
50
+ end
51
+ raise SHACL::Error, "Constraint Component has no validator" unless validator
52
+
53
+ operands << [:validator, validator]
54
+
55
+ new(*operands, **options)
56
+ end
57
+
58
+ # Extract the NCName tail of an IRI as a symbol.
59
+ #
60
+ # @param [RDF::URI] uri
61
+ # @return [Symbol]
62
+ def ncname(uri)
63
+ uri.to_s.match(/(\w+)$/).to_s.to_sym
64
+ end
65
+ end
66
+ end
67
+ 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,28 +15,204 @@ 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
+
44
+ # Parameters to components.
45
+ PARAMETERS = {
46
+ and: {class: :AndConstraintComponent},
47
+ class: {
48
+ class: :ClassConstraintComponent,
49
+ nodeKind: :IRI,
50
+ },
51
+ closed: {
52
+ class: :ClosedConstraintComponent,
53
+ datatype: RDF::XSD.boolean,
54
+ },
55
+ datatype: {
56
+ class: :DatatypeConstraintComponent,
57
+ nodeKind: :IRI,
58
+ maxCount: 1,
59
+ },
60
+ disjoint: {
61
+ class: :DisjointConstraintComponent,
62
+ nodeKind: :IRI,
63
+ },
64
+ equals: {
65
+ class: :EqualsConstraintComponent,
66
+ nodeKind: :IRI,
67
+ },
68
+ expression: {class: :ExpressionConstraintComponent},
69
+ flags: {
70
+ class: :PatternConstraintComponent,
71
+ datatype: RDF::XSD.string,
72
+ optional: true
73
+ },
74
+ hasValue: {
75
+ class: :HasValueConstraintComponent,
76
+ nodeKind: :IRIOrLiteral,
77
+ },
78
+ ignoredProperties: {
79
+ class: :ClosedConstraintComponent,
80
+ nodeKind: :IRI, # Added
81
+ optional: true,
82
+ },
83
+ in: {
84
+ class: :InConstraintComponent,
85
+ nodeKind: :IRIOrLiteral,
86
+ #maxCount: 1, # List internalized
87
+ },
88
+ languageIn: {
89
+ class: :LanguageInConstraintComponent,
90
+ datatype: RDF::XSD.string, # Added
91
+ #maxCount: 1, # List internalized
92
+ },
93
+ lessThan: {
94
+ class: :LessThanConstraintComponent,
95
+ nodeKind: :IRI,
96
+ },
97
+ lessThanOrEquals: {
98
+ class: :LessThanOrEqualsConstraintComponent,
99
+ nodeKind: :IRI,
100
+ },
101
+ maxCount: {
102
+ class: :MaxCountConstraintComponent,
103
+ datatype: RDF::XSD.integer,
104
+ maxCount: 1,
105
+ },
106
+ maxExclusive: {
107
+ class: :MaxExclusiveConstraintComponent,
108
+ maxCount: 1,
109
+ nodeKind: :Literal,
110
+ },
111
+ maxInclusive: {
112
+ class: :MaxInclusiveConstraintComponent,
113
+ maxCount: 1,
114
+ nodeKind: :Literal,
115
+ },
116
+ maxLength: {
117
+ class: :MaxLengthConstraintComponent,
118
+ datatype: RDF::XSD.integer,
119
+ maxCount: 1,
120
+ },
121
+ minCount: {
122
+ class: :MinCountConstraintComponent,
123
+ datatype: RDF::XSD.integer,
124
+ maxCount: 1,
125
+ },
126
+ minExclusive: {
127
+ class: :MinExclusiveConstraintComponent,
128
+ maxCount: 1,
129
+ nodeKind: :Literal,
130
+ },
131
+ minInclusive: {
132
+ class: :MinInclusiveConstraintComponent,
133
+ maxCount: 1,
134
+ nodeKind: :Literal,
135
+ },
136
+ minLength: {
137
+ class: :MinLengthConstraintComponent,
138
+ datatype: RDF::XSD.integer,
139
+ maxCount: 1,
140
+ },
141
+ node: {class: :NodeConstraintComponent},
142
+ nodeKind: {
143
+ class: :NodeKindConstraintComponent,
144
+ in: %i(BlankNode IRI Literal BlankNodeOrIRI BlankNodeOrLiteral IRIOrLiteral),
145
+ maxCount: 1,
146
+ },
147
+ not: {class: :NotConstraintComponent},
148
+ or: {class: :OrConstraintComponent},
149
+ pattern: {
150
+ class: :PatternConstraintComponent,
151
+ datatype: RDF::XSD.string,
152
+ },
153
+ property: {class: :PropertyConstraintComponent},
154
+ qualifiedMaxCount: {
155
+ class: :QualifiedValueConstraintComponent,
156
+ datatype: RDF::XSD.integer,
157
+ },
158
+ qualifiedValueShape: {
159
+ class: :QualifiedValueConstraintComponent,
160
+ },
161
+ qualifiedValueShapesDisjoint: {
162
+ class: :QualifiedValueConstraintComponent,
163
+ datatype: RDF::XSD.boolean,
164
+ optional: true,
165
+ },
166
+ qualifiedMinCount: {
167
+ class: :QualifiedValueConstraintComponent,
168
+ datatype: RDF::XSD.integer
169
+ },
170
+ sparql: {class: :SPARQLConstraintComponent},
171
+ uniqueLang: {
172
+ class: :UniqueLangConstraintComponent,
173
+ datatype: RDF::XSD.boolean,
174
+ maxCount: 1,
175
+ },
176
+ xone: {class: :XoneConstraintComponent},
177
+ }
178
+
38
179
  ## Class methods
39
180
  class << self
181
+ # Add parameters and class def from a SPARQL-based Constraint Component
182
+ #
183
+ # @param [RDF::URI] cls The URI of the constraint component.
184
+ # @param [Hash{Symbol => Hash}] parameters Definitions of mandatory and optional parameters for this component.
185
+ def add_component(cls, parameters)
186
+ # Remember added paraemters.
187
+ # FIXME: should merge parameters
188
+ @added_parameters = (@added_parameters || {}).merge(parameters)
189
+ # Rebuild
190
+ @params = @component_params = nil
191
+ end
192
+
193
+ # Defined parameters for components, which may be supplemented by SPARQL-based Constraint Components. A parameter may be mapped to more than one component class.
194
+ #
195
+ # @return [Hash{Symbol => Hash}] Returns each parameter referencing the component classes it is used in, and the property validators for values of that parameter.
196
+ def params
197
+ @params ||= PARAMETERS.merge(@added_parameters || {})
198
+ end
199
+
200
+ # Constraint Component classes indexed to their mandatory and optional parameters, which may be supplemented by SPARQL-based Constraint Components.
201
+ #
202
+ # @return [Hash{Symbol => Hash}]
203
+ # Returns a hash relating each component URI to its optional and mandatory parameters.
204
+ def component_params
205
+ @component_params ||= params.inject({}) do |memo, (param, properties)|
206
+ memo.merge(Array(properties[:class]).inject(memo) do |mem, cls|
207
+ entry = mem.fetch(cls, {})
208
+ param_type = properties[:optional] ? :optional : :mandatory
209
+ entry[param_type] ||= []
210
+ entry[param_type] << param
211
+ mem.merge(cls => entry)
212
+ end)
213
+ end
214
+ end
215
+
40
216
  ##
41
217
  # Creates an operator instance from a parsed SHACL representation
42
218
  # @param [Hash] operator
@@ -45,52 +221,143 @@ module SHACL::Algebra
45
221
  # @return [Operator]
46
222
  def from_json(operator, **options)
47
223
  operands = []
224
+
225
+ # Node options used to instantiate the relevant class instance.
48
226
  node_opts = options.dup
227
+
228
+ # Node Options and operands on shape or node, which are not Constraint Component Parameters
49
229
  operator.each do |k, v|
50
- next if v.nil?
230
+ k = k.to_sym
231
+ next if v.nil? || params.include?(k)
51
232
  case k
52
233
  # 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'
234
+ when :id then node_opts[:id] = iri(v, vocab: false, **options)
235
+ when :path then node_opts[:path] = parse_path(v, **options)
236
+ when :property
75
237
  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'
238
+ when :severity then node_opts[:severity] = iri(v, **options)
239
+ when :targetClass then node_opts[:targetClass] = as_array(v).map {|vv| iri(vv, **options)}
240
+ when :targetNode
82
241
  node_opts[:targetNode] = as_array(v).map do |vv|
83
242
  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)
243
+ end
244
+ when :targetObjectsOf then node_opts[:targetObjectsOf] = as_array(v).map {|vv| iri(vv, **options)}
245
+ when :targetSubjectsOf then node_opts[:targetSubjectsOf] = as_array(v).map {|vv| iri(vv, **options)}
246
+ when :type then node_opts[:type] = as_array(v).map {|vv| iri(vv, **options)}
91
247
  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)
248
+ if BUILTIN_KEYS.include?(k)
249
+ # Add as a plain option otherwise
250
+ node_opts[k] = to_rdf(k, v, **options)
251
+ end
252
+ end
253
+ end
254
+
255
+ # Node Options and operands on shape or node, which are Constraint Component Parameters.
256
+ # 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.
257
+ used_components = {}
258
+ operator.each do |k, v|
259
+ k = k.to_sym
260
+ next if v.nil? || !params.include?(k)
261
+ param_props = params[k]
262
+ param_classes = Array(param_props[:class])
263
+
264
+ # Keep track of components which have been used.
265
+ param_classes.each {|cls| used_components[cls] ||= {}}
266
+
267
+ # Check parameter constraints
268
+ v = as_array(v)
269
+ if param_props[:maxCount] && v.length > param_props[:maxCount]
270
+ raise SHACL::Error, "Property #{k} on #{self.const_get(:NAME)} has too many values: #{v.inspect}"
271
+ end
272
+
273
+ # If an optional parameter exists without corresponding mandatory parameters on a given shape, raise a SHACL::Error.
274
+ #
275
+ # Records any instances of components which are created to re-attach non-primary parameters after all operators are processed.
276
+ instances = case k
277
+ # List properties
278
+ when :node
279
+ as_array(v).map {|vv| NodeShape.from_json(vv, **options)}
280
+ when :property
281
+ as_array(v).map {|vv| PropertyShape.from_json(vv, **options)}
282
+ when :sparql
283
+ as_array(v).map {|vv| SPARQLConstraintComponent.from_json(vv, **options)}
284
+ else
285
+ # Process parameter values based on nodeKind, in, and datatype.
286
+ elements = if param_props[:nodeKind]
287
+ case param_props[:nodeKind]
288
+ when :IRI
289
+ v.map {|vv| iri(vv, **options)}
290
+ when :Literal
291
+ v.map do |vv|
292
+ vv.is_a?(Hash) ?
293
+ from_expanded_value(vv, **options) :
294
+ RDF::Literal(vv)
295
+ end
296
+ when :IRIOrLiteral
297
+ to_rdf(k, v, **options)
298
+ end
299
+ elsif param_props[:in]
300
+ v.map do |vv|
301
+ iri(vv, **options) if param_props[:in].include?(vv.to_sym)
302
+ end
303
+ elsif param_props[:datatype]
304
+ v.map {|vv| RDF::Literal(vv, datatype: param_props[:datatype])}
305
+ else
306
+ v.map {|vv| SHACL::Algebra.from_json(vv, **options)}
307
+ end
308
+
309
+ # Builtins are added as options to the operator, otherwise, they are class instances of constraint components added as operators.
310
+ if BUILTIN_KEYS.include?(k)
311
+ node_opts[k] = elements
312
+ [] # No instances created
313
+ else
314
+ klass = SHACL::Algebra.const_get(Array(param_props[:class]).first)
315
+
316
+ name = klass.const_get(:NAME)
317
+ # If the key `k` is the same as the NAME of the class, create the instance with the defined element values.
318
+ if name == k
319
+ param_classes.each do |cls|
320
+ # Add `k` as a mandatory parameter fulfilled
321
+ (used_components[cls][:mandatory_parameters] ||= []) << k
322
+ end
323
+
324
+ # Instantiate the compoent
325
+ elements.map {|e| klass.new(*e, **options.dup)}
326
+ else
327
+ # Add non-primary parameters for subsequent insertion
328
+ param_classes.each do |cls|
329
+ # Add `k` as a mandatory parameter fulfilled if it is so defined
330
+ (used_components[cls][:mandatory_parameters] ||= []) << k unless
331
+ params[k][:optional]
332
+
333
+ # Add parameter as S-Expression operand
334
+ (used_components[cls][:parameters] ||= []) << elements.unshift(k)
335
+ end
336
+ [] # No instances created
337
+ end
338
+ end
339
+ end
340
+
341
+ # Record the instances created by class and its operands
342
+ param_classes.each do |cls|
343
+ used_components[cls][:instances] = instances
344
+ end
345
+
346
+ # FIXME: Only add instances when all mandatory parameters are present.
347
+ operands.push(*instances)
348
+ end
349
+
350
+ # Append any parameters to the used components
351
+ used_components.each do |cls, props|
352
+ instances = props[:instances]
353
+ next unless instances # BUILTINs
354
+
355
+ parameters = props.fetch(:parameters, [])
356
+ instances.each do |op|
357
+ parameters.each do |param|
358
+ # Note the potential that the parameter gets added twice, if there are multiple classes for both the primary and secondary paramters.
359
+ op.operands << param
360
+ end
94
361
  end
95
362
  end
96
363
 
@@ -189,7 +456,7 @@ module SHACL::Algebra
189
456
  end
190
457
 
191
458
  ##
192
- # Parse the "patH" attribute into a SPARQL Property Path and evaluate to find related nodes.
459
+ # Parse the "path" attribute into a SPARQL Property Path and evaluate to find related nodes.
193
460
  #
194
461
  # @param [Object] path
195
462
  # @return [RDF::URI, SPARQL::Algebra::Expression]
@@ -282,8 +549,9 @@ module SHACL::Algebra
282
549
  raise NotImplemented
283
550
  end
284
551
 
552
+ # Create structure for serializing this component/shape, beginning with its cononical name.
285
553
  def to_sxp_bin
286
- expressions = ALL_KEYS.inject([self.class.const_get(:NAME)]) do |memo, sym|
554
+ expressions = BUILTIN_KEYS.inject([self.class.const_get(:NAME)]) do |memo, sym|
287
555
  @options[sym] ? memo.push([sym, *@options[sym]]) : memo
288
556
  end + operands
289
557
 
@@ -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