shacl 0.2.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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"),