shacl 0.2.1 → 0.4.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.
@@ -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,35 +1,41 @@
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]
21
+ min_count = params[:qualifiedMinCount]
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
23
29
 
24
30
  count = results.select(&:conform?).length
25
31
  log_debug(NAME, depth: depth) {"#{count}/#{results} conforming shapes"}
26
- if count < min_count
32
+ if min_count && count < min_count.to_i
27
33
  not_satisfied(focus: node, path: path,
28
34
  message: "only #{count} conforming values, requires at least #{min_count}",
29
35
  resultSeverity: options.fetch(:severity),
30
36
  component: RDF::Vocab::SHACL.QualifiedMinCountConstraintComponent,
31
37
  depth: depth, **options)
32
- elsif count > max_count
38
+ elsif max_count && count > max_count.to_i
33
39
  not_satisfied(focus: node, path: path,
34
40
  message: "#{count} conforming values, requires at most #{max_count}",
35
41
  resultSeverity: options.fetch(:severity),
@@ -37,7 +43,7 @@ module SHACL::Algebra
37
43
  depth: depth, **options)
38
44
  else
39
45
  satisfy(focus: node, path: path,
40
- message: "#{min_count} <= #{count} <= #{max_count} values conform",
46
+ message: "#{min_count.to_i} <= #{count} <= #{max_count || 'inf'} values conform",
41
47
  component: RDF::Vocab::SHACL.QualifiedMinCountConstraintComponent,
42
48
  depth: depth, **options)
43
49
  end
@@ -81,6 +81,7 @@ module SHACL::Algebra
81
81
  # @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
82
82
  # @return [Array<SHACL::ValidationResult>]
83
83
  def builtin_datatype(datatype, node, path, value_nodes, **options)
84
+ datatype = datatype.first if datatype.is_a?(Array)
84
85
  value_nodes.map do |n|
85
86
  has_datatype = n.literal? && n.datatype == datatype && n.valid?
86
87
  satisfy(focus: node, path: path,
@@ -140,6 +141,7 @@ module SHACL::Algebra
140
141
  # @param [Array<RDF::Term>] value_nodes
141
142
  # @return [Array<SHACL::ValidationResult>]
142
143
  def builtin_equals(property, node, path, value_nodes, **options)
144
+ property = property.first if property.is_a?(Array)
143
145
  equal_nodes = graph.query({subject: node, predicate: property}).objects
144
146
  (value_nodes.map do |n|
145
147
  has_value = equal_nodes.include?(n)
@@ -179,6 +181,7 @@ module SHACL::Algebra
179
181
  # @param [Array<RDF::Term>] value_nodes
180
182
  # @return [Array<SHACL::ValidationResult>]
181
183
  def builtin_hasValue(term, node, path, value_nodes, **options)
184
+ term = term.first if term.is_a?(Array)
182
185
  has_value = value_nodes.include?(term)
183
186
  [satisfy(focus: node, path: path,
184
187
  message: "is#{' not' unless has_value} the value #{term.to_sxp}",
@@ -260,7 +263,7 @@ module SHACL::Algebra
260
263
  # @param [Array<RDF::Term>] value_nodes
261
264
  # @return [Array<SHACL::ValidationResult>]
262
265
  def builtin_maxExclusive(term, node, path, value_nodes, **options)
263
- compare(:<, [term], node, path, value_nodes,
266
+ compare(:<, term, node, path, value_nodes,
264
267
  RDF::Vocab::SHACL.MaxExclusiveConstraintComponent, **options)
265
268
  end
266
269
 
@@ -282,7 +285,7 @@ module SHACL::Algebra
282
285
  # @param [Array<RDF::Term>] value_nodes
283
286
  # @return [Array<SHACL::ValidationResult>]
284
287
  def builtin_maxInclusive(term, node, path, value_nodes, **options)
285
- compare(:<=, [term], node, path, value_nodes,
288
+ compare(:<=, term, node, path, value_nodes,
286
289
  RDF::Vocab::SHACL.MaxInclusiveConstraintComponent, **options)
287
290
  end
288
291
 
@@ -294,6 +297,7 @@ module SHACL::Algebra
294
297
  # @param [Array<RDF::Term>] value_nodes
295
298
  # @return [Array<SHACL::ValidationResult>]
296
299
  def builtin_maxLength(term, node, path, value_nodes, **options)
300
+ term = term.first if term.is_a?(Array)
297
301
  value_nodes.map do |n|
298
302
  compares = !n.node? && n.to_s.length <= term.to_i
299
303
  satisfy(focus: node, path: path,
@@ -323,7 +327,7 @@ module SHACL::Algebra
323
327
  # @param [Array<RDF::Term>] value_nodes
324
328
  # @return [Array<SHACL::ValidationResult>]
325
329
  def builtin_minExclusive(term, node, path, value_nodes, **options)
326
- compare(:>, [term], node, path, value_nodes,
330
+ compare(:>, term, node, path, value_nodes,
327
331
  RDF::Vocab::SHACL.MinExclusiveConstraintComponent, **options)
328
332
  end
329
333
 
@@ -341,11 +345,11 @@ module SHACL::Algebra
341
345
  #
342
346
  # @param [RDF::URI] term the term is used to compare each value node.
343
347
  # @param [RDF::Term] node the focus node
344
- # @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus nod to the value nodes.
348
+ # @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
345
349
  # @param [Array<RDF::Term>] value_nodes
346
350
  # @return [Array<SHACL::ValidationResult>]
347
351
  def builtin_minInclusive(term, node, path, value_nodes, **options)
348
- compare(:>=, [term], node, path, value_nodes,
352
+ compare(:>=, term, node, path, value_nodes,
349
353
  RDF::Vocab::SHACL.MinInclusiveConstraintComponent, **options)
350
354
  end
351
355
 
@@ -357,6 +361,7 @@ module SHACL::Algebra
357
361
  # @param [Array<RDF::Term>] value_nodes
358
362
  # @return [Array<SHACL::ValidationResult>]
359
363
  def builtin_minLength(term, node, path, value_nodes, **options)
364
+ term = term.first if term.is_a?(Array)
360
365
  value_nodes.map do |n|
361
366
  compares = !n.node? && n.to_s.length >= term.to_i
362
367
  satisfy(focus: node, path: path,
@@ -402,6 +407,7 @@ module SHACL::Algebra
402
407
  # @param [Array<RDF::Term>] value_nodes
403
408
  # @return [Array<SHACL::ValidationResult>]
404
409
  def builtin_nodeKind(term, node, path, value_nodes, **options)
410
+ term = term.first if term.is_a?(Array)
405
411
  value_nodes.map do |n|
406
412
  compares = NODE_KIND_COMPARE.fetch(n.class, []).include?(term)
407
413
  satisfy(focus: node, path: path,
@@ -413,46 +419,11 @@ module SHACL::Algebra
413
419
  end.flatten.compact
414
420
  end
415
421
 
416
- # Specifies a regular expression that each value node matches to satisfy the condition.
417
- #
418
- # @example
419
- # ex:PatternExampleShape
420
- # a sh:NodeShape ;
421
- # sh:targetNode ex:Bob, ex:Alice, ex:Carol ;
422
- # sh:property [
423
- # sh:path ex:bCode ;
424
- # sh:pattern "^B" ; # starts with 'B'
425
- # sh:flags "i" ; # Ignore case
426
- # ] .
427
- #
428
- # @param [RDF::URI] pattern A regular expression that all value nodes need to match.
429
- # @param [RDF::Term] node the focus node
430
- # @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes..
431
- # @param [Array<RDF::Term>] value_nodes
432
- # @return [Array<SHACL::ValidationResult>]
433
- def builtin_pattern(pattern, node, path, value_nodes, **options)
434
- flags = options[:flags].to_s
435
- regex_opts = 0 |
436
- regex_opts |= Regexp::MULTILINE if flags.include?(?m)
437
- regex_opts |= Regexp::IGNORECASE if flags.include?(?i)
438
- regex_opts |= Regexp::EXTENDED if flags.include?(?x)
439
- pat = Regexp.new(pattern, regex_opts)
440
-
441
- value_nodes.map do |n|
442
- compares = !n.node? && pat.match?(n.to_s)
443
- satisfy(focus: node, path: path,
444
- value: n,
445
- message: "is#{' not' unless compares} a match #{pat.inspect}",
446
- resultSeverity: (options.fetch(:severity) unless compares),
447
- component: RDF::Vocab::SHACL.PatternConstraintComponent,
448
- **options)
449
- end.flatten.compact
450
- end
451
-
452
422
  protected
453
423
 
454
424
  # Common comparison logic for lessThan, lessThanOrEqual, max/minInclusive/Exclusive
455
425
  def compare(method, terms, node, path, value_nodes, component, **options)
426
+ terms = [terms] unless terms.is_a?(Array)
456
427
  value_nodes.map do |left|
457
428
  results = terms.map do |right|
458
429
  case left
@@ -0,0 +1,160 @@
1
+ require_relative "shape"
2
+ require 'sparql'
3
+ require 'rdf/aggregate_repo'
4
+
5
+ module SHACL::Algebra
6
+ ##
7
+ class SPARQLConstraintComponent < ConstraintComponent
8
+ NAME = :sparql
9
+
10
+ # SPARQL Operators prohibited from being used in expression.
11
+ UNSUPPORTED_SPARQL_OPERATORS = [
12
+ SPARQL::Algebra::Operator::Minus,
13
+ SPARQL::Algebra::Operator::Service,
14
+ SPARQL::Algebra::Operator::Table,
15
+ ]
16
+
17
+ # Potentially pre-bound variables.
18
+ PRE_BOUND = %i(currentShape shapesGraph PATH this value)
19
+
20
+ # Validates the specified `property` within `graph`, a list of {ValidationResult}.
21
+ #
22
+ # A property conforms the nodes found by evaluating it's `path` all conform.
23
+ #
24
+ # Last operand is the parsed query. Bound variables are added as a table entry joined to the query.
25
+ #
26
+ # @param [RDF::Term] node focus node
27
+ # @param [RDF::URI, SPARQL::Algebra::Expression] path the property path from the focus node to the value nodes.
28
+ # @param [Hash{Symbol => Object}] options
29
+ # @return [Array<SHACL::ValidationResult>]
30
+ # Returns a validation result for each value node.
31
+ def conforms(node, path: nil, depth: 0, **options)
32
+ return [] if deactivated?
33
+ options = {severity: RDF::Vocab::SHACL.Violation}.merge(options)
34
+ log_debug(NAME, depth: depth) {SXP::Generator.string({node: node}.to_sxp_bin)}
35
+
36
+ # Aggregate repo containing both data-graph (as the default) and shapes-graph, named by it's IRI
37
+ aggregate = RDF::AggregateRepo.new(graph.data, shapes_graph.data) do |ag|
38
+ ag.default false
39
+ ag.named shapes_graph.graph_name if shapes_graph.graph_name
40
+ end
41
+
42
+ aggregate.default_graph
43
+ bindings = RDF::Query::Solution.new({
44
+ currentShape: options[:shape],
45
+ shapesGraph: shapes_graph.graph_name,
46
+ PATH: path,
47
+ this: node,
48
+ }.compact)
49
+ solutions = operands.last.execute(aggregate,
50
+ bindings: bindings,
51
+ depth: depth + 1,
52
+ logger: (@logger || @options[:logger]),
53
+ **options)
54
+ if solutions.empty?
55
+ satisfy(focus: node, path: path,
56
+ message: @options.fetch(:message, "node conforms to SPARQL component"),
57
+ component: RDF::Vocab::SHACL.SPARQLConstraintComponent,
58
+ depth: depth, **options)
59
+ else
60
+ solutions.map do |solution|
61
+ not_satisfied(focus: node, path: (path || solution[:path]),
62
+ value: (solution[:value] || node),
63
+ message: @options.fetch(:message, "node does not coform to SPARQL component"),
64
+ resultSeverity: options.fetch(:severity),
65
+ component: RDF::Vocab::SHACL.SPARQLConstraintComponent,
66
+ depth: depth, **options)
67
+ end
68
+ end
69
+ end
70
+
71
+ # All keys associated with shapes which are set in options
72
+ #
73
+ # @return [Array<Symbol>]
74
+ BUILTIN_KEYS = %i(
75
+ type label name comment description deactivated severity
76
+ message path
77
+ ask select
78
+ declare namespace prefix prefixes select ask
79
+ ).freeze
80
+
81
+ # Class Methods
82
+ class << self
83
+ ##
84
+ # Creates an operator instance from a parsed SHACL representation.
85
+ #
86
+ # Special case for SPARQLComponenet due to general recursion.
87
+ #
88
+ # @param [Hash] operator
89
+ # @param [Hash] options ({})
90
+ # @option options [Hash{String => RDF::URI}] :prefixes
91
+ # @return [Operator]
92
+ def from_json(operator, **options)
93
+ prefixes, query = [], ""
94
+ operands = []
95
+ node_opts = options.dup
96
+ operator.each do |k, v|
97
+ next if v.nil?
98
+ case k
99
+ # List properties
100
+ when 'path' then node_opts[:path] = parse_path(v, **options)
101
+ when 'prefixes'
102
+ prefixes = extract_prefixes(v)
103
+ when 'severity' then node_opts[:severity] = iri(v, **options)
104
+ when 'type' then node_opts[:type] = as_array(v).map {|vv| iri(vv, **options)} if v
105
+ else
106
+ node_opts[k.to_sym] = to_rdf(k.to_sym, v, **options) if BUILTIN_KEYS.include?(k.to_sym)
107
+ end
108
+ end
109
+
110
+ query_string = prefixes.join("\n") + node_opts[:select] || node_opts[:ask]
111
+ query = SPARQL.parse(query_string)
112
+
113
+ options[:logger].info("#{NAME} SXP: #{query.to_sxp}") if options[:logger]
114
+
115
+ # Queries have restrictions
116
+ operators = query.descendants.to_a.unshift(query)
117
+
118
+ if node_opts[:ask] && !operators.any? {|op| op.is_a?(SPARQL::Algebra::Operator::Ask)}
119
+ raise SHACL::Error, "Ask query must have ask operator"
120
+ elsif node_opts[:select] && !operators.any? {|op| op.is_a?(SPARQL::Algebra::Operator::Project)}
121
+ raise SHACL::Error, "Select query must have project operator"
122
+ end
123
+
124
+ uh_oh = (operators.map(&:class) & UNSUPPORTED_SPARQL_OPERATORS).map {|c| c.const_get(:NAME)}
125
+
126
+ unless uh_oh.empty?
127
+ raise SHACL::Error, "Query must not include operators #{uh_oh.to_sxp}: #{query_string}"
128
+ end
129
+
130
+ # Additionally, queries must not bind to special variables
131
+ operators.select {|op| op.is_a?(SPARQL::Algebra::Operator::Extend)}.each do |extend|
132
+ if extend.operands.first.any? {|v, e| PRE_BOUND.include?(v.to_sym)}
133
+ raise SHACL::Error, "Query must not bind pre-bound variables: #{query_string}"
134
+ end
135
+ end
136
+
137
+ operands << query
138
+ new(*operands, **node_opts)
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ # Returns an array of prefix definitions
145
+ def self.extract_prefixes(value)
146
+ case value
147
+ when Hash
148
+ ret = []
149
+ # Recursively extract decllarations
150
+ extract_prefixes(value.fetch('imports', nil)) +
151
+ as_array(value.fetch('declare', [])).map do |decl|
152
+ pfx, ns = decl['prefix'], decl['namespace']
153
+ "PREFIX #{pfx}: <#{ns}>"
154
+ end
155
+ when Array then value.map {|v| extract_prefixes(v)}.flatten
156
+ else []
157
+ end
158
+ end
159
+ end
160
+ end
@@ -1,6 +1,6 @@
1
1
  module SHACL::Algebra
2
2
  ##
3
- class Xone < Operator
3
+ class XoneConstraintComponent < ConstraintComponent
4
4
  NAME = :xone
5
5
 
6
6
  ##
@@ -29,7 +29,8 @@ module SHACL::Algebra
29
29
  # ]
30
30
  # ) .
31
31
  #
32
- # @param [RDF::Term] node
32
+ # @param [RDF::Term] node focus node
33
+ # @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property
33
34
  # @param [Hash{Symbol => Object}] options
34
35
  # @return [Array<SHACL::ValidationResult>]
35
36
  def conforms(node, path: nil, depth: 0, **options)
data/lib/shacl/algebra.rb CHANGED
@@ -1,29 +1,46 @@
1
1
  $:.unshift(File.expand_path("../..", __FILE__))
2
2
  require 'sxp'
3
3
  require_relative "algebra/operator"
4
+ require_relative "algebra/constraint_component"
4
5
 
5
6
  module SHACL
6
7
  # Based on the SPARQL Algebra, operators for executing a patch
7
8
  module Algebra
8
- autoload :And, 'shacl/algebra/and.rb'
9
- autoload :Datatype, 'shacl/algebra/datatype.rb'
10
- autoload :Klass, 'shacl/algebra/klass.rb'
11
- autoload :NodeShape, 'shacl/algebra/node_shape.rb'
12
- autoload :Not, 'shacl/algebra/not.rb'
13
- autoload :Or, 'shacl/algebra/or.rb'
14
- autoload :PropertyShape, 'shacl/algebra/property_shape.rb'
15
- autoload :QualifiedValueShape, 'shacl/algebra/qualified_value_shape.rb'
16
- autoload :Shape, 'shacl/algebra/shape.rb'
17
- autoload :Xone, 'shacl/algebra/xone.rb'
9
+ autoload :AndConstraintComponent, 'shacl/algebra/and.rb'
10
+ autoload :NodeShape, 'shacl/algebra/node_shape.rb'
11
+ autoload :NotConstraintComponent, 'shacl/algebra/not.rb'
12
+ autoload :OrConstraintComponent, 'shacl/algebra/or.rb'
13
+ autoload :PatternConstraintComponent, 'shacl/algebra/pattern.rb'
14
+ autoload :PropertyShape, 'shacl/algebra/property_shape.rb'
15
+ autoload :QualifiedMaxCountConstraintComponent, 'shacl/algebra/qualified_value.rb'
16
+ autoload :QualifiedMinCountConstraintComponent, 'shacl/algebra/qualified_value.rb'
17
+ autoload :QualifiedValueConstraintComponent, 'shacl/algebra/qualified_value.rb'
18
+ autoload :Shape, 'shacl/algebra/shape.rb'
19
+ autoload :SPARQLConstraintComponent, 'shacl/algebra/sparql_constraint.rb'
20
+ autoload :XoneConstraintComponent, 'shacl/algebra/xone.rb'
18
21
 
19
22
  def self.from_json(operator, **options)
20
- raise ArgumentError, "from_json: operator not a Hash: #{operator.inspect}" unless operator.is_a?(Hash)
23
+ raise SHACL::Error, "from_json: operator not a Hash: #{operator.inspect}" unless operator.is_a?(Hash)
24
+
25
+ # If operator is a hash containing @list, it is a single array value.
26
+ # Note: context does not use @container: @list on this terms to preserve cardinality expectations
27
+ return operator['@list'].map {|e| from_json(e, **options)} if operator.key?('@list')
28
+
21
29
  type = operator.fetch('type', [])
22
- type << (operator["path"] ? 'PropertyShape' : 'NodeShape') if type.empty?
30
+ if type.empty?
31
+ type << if operator["path"]
32
+ 'PropertyShape'
33
+ elsif operator['nodeValidator'] || operator['propertyValidator'] || operator['validator']
34
+ 'ConstraintComponent'
35
+ else
36
+ 'NodeShape'
37
+ end
38
+ end
23
39
  klass = case
24
40
  when type.include?('NodeShape') then NodeShape
25
41
  when type.include?('PropertyShape') then PropertyShape
26
- else raise ArgumentError, "from_json: unknown type #{type.inspect}"
42
+ when type.include?('ConstraintComponent') then ConstraintComponent
43
+ else raise SHACL::Error, "from_json: unknown type #{type.inspect}"
27
44
  end
28
45
 
29
46
  klass.from_json(operator, **options)
data/lib/shacl/context.rb CHANGED
@@ -17,20 +17,25 @@ class JSON::LD::Context
17
17
  "equals" => TermDefinition.new("equals", id: "http://www.w3.org/ns/shacl#equals", type_mapping: "@id"),
18
18
  "id" => TermDefinition.new("id", id: "@id", simple: true),
19
19
  "ignoredProperties" => TermDefinition.new("ignoredProperties", id: "http://www.w3.org/ns/shacl#ignoredProperties", type_mapping: "@id", container_mapping: "@list"),
20
+ "imports" => TermDefinition.new("imports", id: "http://www.w3.org/2002/07/owl#imports", type_mapping: "@id"),
20
21
  "in" => TermDefinition.new("in", id: "http://www.w3.org/ns/shacl#in", type_mapping: "@none", container_mapping: "@list"),
21
22
  "inversePath" => TermDefinition.new("inversePath", id: "http://www.w3.org/ns/shacl#inversePath", type_mapping: "@id"),
22
23
  "label" => TermDefinition.new("label", id: "http://www.w3.org/2000/01/rdf-schema#label", simple: true),
23
24
  "languageIn" => TermDefinition.new("languageIn", id: "http://www.w3.org/ns/shacl#languageIn", container_mapping: "@list"),
24
25
  "lessThan" => TermDefinition.new("lessThan", id: "http://www.w3.org/ns/shacl#lessThan", type_mapping: "@id"),
25
26
  "lessThanOrEquals" => TermDefinition.new("lessThanOrEquals", id: "http://www.w3.org/ns/shacl#lessThanOrEquals", type_mapping: "@id"),
27
+ "namespace" => TermDefinition.new("namespace", id: "http://www.w3.org/ns/shacl#namespace", type_mapping: "http://www.w3.org/2001/XMLSchema#anyURI"),
26
28
  "nodeKind" => TermDefinition.new("nodeKind", id: "http://www.w3.org/ns/shacl#nodeKind", type_mapping: "@vocab"),
27
29
  "or" => TermDefinition.new("or", id: "http://www.w3.org/ns/shacl#or", type_mapping: "@id", container_mapping: "@list"),
30
+ "owl" => TermDefinition.new("owl", id: "http://www.w3.org/2002/07/owl#", simple: true, prefix: true),
28
31
  "path" => TermDefinition.new("path", id: "http://www.w3.org/ns/shacl#path", type_mapping: "@none"),
32
+ "prefixes" => TermDefinition.new("prefixes", id: "http://www.w3.org/ns/shacl#prefixes", type_mapping: "@id"),
29
33
  "property" => TermDefinition.new("property", id: "http://www.w3.org/ns/shacl#property", type_mapping: "@id"),
30
34
  "rdfs" => TermDefinition.new("rdfs", id: "http://www.w3.org/2000/01/rdf-schema#", simple: true, prefix: true),
31
35
  "severity" => TermDefinition.new("severity", id: "http://www.w3.org/ns/shacl#severity", type_mapping: "@vocab"),
32
36
  "sh" => TermDefinition.new("sh", id: "http://www.w3.org/ns/shacl#", simple: true, prefix: true),
33
37
  "shacl" => TermDefinition.new("shacl", id: "http://www.w3.org/ns/shacl#", simple: true, prefix: true),
38
+ "sparql" => TermDefinition.new("sparql", id: "http://www.w3.org/ns/shacl#sparql", type_mapping: "@id"),
34
39
  "targetClass" => TermDefinition.new("targetClass", id: "http://www.w3.org/ns/shacl#targetClass", type_mapping: "@id"),
35
40
  "targetNode" => TermDefinition.new("targetNode", id: "http://www.w3.org/ns/shacl#targetNode", type_mapping: "@none"),
36
41
  "type" => TermDefinition.new("type", id: "@type", container_mapping: "@set"),