shacl 0.2.1 → 0.3.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.
@@ -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,37 @@
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
30
  type << (operator["path"] ? 'PropertyShape' : 'NodeShape') if type.empty?
23
31
  klass = case
24
32
  when type.include?('NodeShape') then NodeShape
25
33
  when type.include?('PropertyShape') then PropertyShape
26
- else raise ArgumentError, "from_json: unknown type #{type.inspect}"
34
+ else raise SHACL::Error, "from_json: unknown type #{type.inspect}"
27
35
  end
28
36
 
29
37
  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"),
data/lib/shacl/shapes.rb CHANGED
@@ -9,6 +9,12 @@ module SHACL
9
9
  class Shapes < Array
10
10
  include RDF::Util::Logger
11
11
 
12
+ # The original shapes graph
13
+ #
14
+ # @return [RDF::Graph]
15
+ attr_reader :shapes_graph
16
+
17
+
12
18
  # The graphs which have been loaded as shapes
13
19
  #
14
20
  # @return [Array<RDF::URI>]
@@ -28,6 +34,7 @@ module SHACL
28
34
  #
29
35
  # @param [RDF::Graph] graph
30
36
  # @param [Array<RDF::URI>] loaded_graphs = []
37
+ # The graphs which have been loaded as shapes
31
38
  # @param [Hash{Symbol => Object}] options
32
39
  # @return [Shapes]
33
40
  # @raise [SHACL::Error]
@@ -38,8 +45,17 @@ module SHACL
38
45
  while (imports = graph.query({predicate: RDF::OWL.imports}).map(&:object)).count > import_count
39
46
  # Load each imported graph
40
47
  imports.each do |ref|
41
- graph.load(imports)
42
- loaded_graphs << ref
48
+ # Don't try import if the import subject is already in the graph
49
+ unless graph.subject?(ref)
50
+ begin
51
+ options[:logger].info('Shapes') {"load import #{ref}"} if options[:logger].respond_to?(:info)
52
+ graph.load(ref)
53
+ loaded_graphs << ref
54
+ rescue IOError => e
55
+ # Skip import
56
+ options[:logger].warn('Shapes') {"load import #{ref}"} if options[:logger].respond_to?(:warn)
57
+ end
58
+ end
43
59
  import_count += 1
44
60
  end
45
61
  end
@@ -52,6 +68,7 @@ module SHACL
52
68
  # Create an array of the framed shapes
53
69
  shapes = self.new(shape_json.map {|o| Algebra.from_json(o, **options)})
54
70
  shapes.instance_variable_set(:@shape_json, shape_json)
71
+ shapes.instance_variable_set(:@shapes_graph, graph)
55
72
  shapes
56
73
  end
57
74
 
@@ -65,11 +82,11 @@ module SHACL
65
82
  # @raise [SHACL::Error]
66
83
  def self.from_queryable(queryable, **options)
67
84
  # Query queryable to find one ore more shapes graphs
68
- graphs = queryable.query({predicate: RDF::Vocab::SHACL.shapesGraph}).objects
69
- graph = RDF::Graph.new do |g|
70
- graphs.each {|iri| g.load(iri)}
85
+ graph_names = queryable.query({predicate: RDF::Vocab::SHACL.shapesGraph}).objects
86
+ graph = RDF::Graph.new(graph_name: graph_names.first, data: RDF::Repository.new) do |g|
87
+ graph_names.each {|iri| g.load(iri, graph_name: graph_names.first)}
71
88
  end
72
- from_graph(graph, loaded_graphs: graphs, **options)
89
+ from_graph(graph, loaded_graphs: graph_names, **options)
73
90
  end
74
91
 
75
92
  ##
@@ -80,12 +97,18 @@ module SHACL
80
97
  # @param [Hash{Symbol => Object}] options
81
98
  # @option options [RDF::Term] :focus
82
99
  # An explicit focus node, overriding any defined on the top-level shaps.
100
+ # @option options [Logger, #write, #<<] :logger
101
+ # Record error/info/debug output
83
102
  # @return [SHACL::ValidationReport]
84
103
  def execute(graph, depth: 0, **options)
85
104
  self.each do |shape|
86
105
  shape.graph = graph
106
+ shape.shapes_graph = shapes_graph
87
107
  shape.each_descendant do |op|
88
- op.graph = graph
108
+ op.instance_variable_set(:@logger, options[:logger]) if
109
+ options[:logger] && op.respond_to?(:execute)
110
+ op.graph = graph if op.respond_to?(:graph=)
111
+ op.shapes_graph = shapes_graph if op.respond_to?(:shapes_graph=)
89
112
  end
90
113
  end
91
114
 
@@ -115,11 +138,12 @@ module SHACL
115
138
  "id": "@id",
116
139
  "type": {"@id": "@type", "@container": "@set"},
117
140
  "@vocab": "http://www.w3.org/ns/shacl#",
141
+ "owl": "http://www.w3.org/2002/07/owl#",
118
142
  "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
119
143
  "shacl": "http://www.w3.org/ns/shacl#",
120
144
  "sh": "http://www.w3.org/ns/shacl#",
121
145
  "xsd": "http://www.w3.org/2001/XMLSchema#",
122
- "and": {"@type": "@id", "@container": "@list"},
146
+ "and": {"@type": "@id"},
123
147
  "annotationProperty": {"@type": "@id"},
124
148
  "class": {"@type": "@id"},
125
149
  "comment": "http://www.w3.org/2000/01/rdf-schema#comment",
@@ -130,30 +154,35 @@ module SHACL
130
154
  "entailment": {"@type": "@id"},
131
155
  "equals": {"@type": "@id"},
132
156
  "ignoredProperties": {"@type": "@id", "@container": "@list"},
157
+ "imports": {"@id": "owl:imports", "@type": "@id"},
133
158
  "in": {"@type": "@none", "@container": "@list"},
134
159
  "inversePath": {"@type": "@id"},
135
160
  "label": "http://www.w3.org/2000/01/rdf-schema#label",
136
161
  "languageIn": {"@container": "@list"},
137
162
  "lessThan": {"@type": "@id"},
138
163
  "lessThanOrEquals": {"@type": "@id"},
164
+ "namespace": {"@type": "xsd:anyURI"},
139
165
  "nodeKind": {"@type": "@vocab"},
140
- "or": {"@type": "@id", "@container": "@list"},
166
+ "or": {"@type": "@id"},
141
167
  "path": {"@type": "@none"},
168
+ "prefixes": {"@type": "@id"},
142
169
  "property": {"@type": "@id"},
143
170
  "severity": {"@type": "@vocab"},
171
+ "sparql": {"@type": "@id"},
144
172
  "targetClass": {"@type": "@id"},
145
173
  "targetNode": {"@type": "@none"},
146
- "xone": {"@type": "@id", "@container": "@list"}
174
+ "xone": {"@type": "@id"}
147
175
  },
148
176
  "and": {},
149
177
  "class": {},
150
178
  "datatype": {},
151
- "in": {},
179
+ "in": {"@embed": "@never"},
152
180
  "node": {},
153
181
  "nodeKind": {},
154
182
  "not": {},
155
183
  "or": {},
156
184
  "property": {},
185
+ "sparql": {},
157
186
  "targetClass": {},
158
187
  "targetNode": {},
159
188
  "targetObjectsOf": {},
@@ -115,26 +115,29 @@ module SHACL
115
115
  block.call(RDF::Statement(subject, RDF::Vocab::SHACL.resultMessage, RDF::Literal(message))) if message
116
116
  end
117
117
 
118
- # Transform a JSON representation of a result, into a native representation
119
- # @param [Hash] input
120
- # @return [ValidationResult]
121
- def self.from_json(input, **options)
122
- input = JSON.parse(input) if input.is_a?(String)
123
- input = JSON::LD::API.compact(input,
124
- "http://github.com/ruby-rdf/shacl/",
125
- expandContext: "http://github.com/ruby-rdf/shacl/")
126
- raise ArgumentError, "Expect report to be a hash" unless input.is_a?(Hash)
127
- result = self.new
118
+ # Class Methods
119
+ class << self
120
+ # Transform a JSON representation of a result, into a native representation
121
+ # @param [Hash] input
122
+ # @return [ValidationResult]
123
+ def from_json(input, **options)
124
+ input = JSON.parse(input) if input.is_a?(String)
125
+ input = JSON::LD::API.compact(input,
126
+ "http://github.com/ruby-rdf/shacl/",
127
+ expandContext: "http://github.com/ruby-rdf/shacl/")
128
+ raise ArgumentError, "Expect report to be a hash" unless input.is_a?(Hash)
129
+ result = self.new
128
130
 
129
- result.focus = Algebra::Operator.to_rdf(:focus, input['focusNode'], base: nil, vocab: false) if input['focusNode']
130
- result.path = Algebra::Operator.parse_path(input['resultPath'], **options) if input['resultPath']
131
- result.resultSeverity = Algebra::Operator.iri(input['resultSeverity'], **options) if input['resultSeverity']
132
- result.component = Algebra::Operator.iri(input['sourceConstraintComponent'], **options) if input['sourceConstraintComponent']
133
- result.shape = Algebra::Operator.iri(input['sourceShape'], **options) if input['sourceShape']
134
- result.value = Algebra::Operator.to_rdf(:value, input['value'], **options) if input['value']
135
- result.details = Algebra::Operator.to_rdf(:details, input['details'], **options) if input['details']
136
- result.message = Algebra::Operator.to_rdf(:message, input['message'], **options) if input['message']
137
- result
131
+ result.focus = Algebra::Operator.to_rdf(:focus, input['focusNode'], base: nil, vocab: false) if input['focusNode']
132
+ result.path = Algebra::Operator.parse_path(input['resultPath'], **options) if input['resultPath']
133
+ result.resultSeverity = Algebra::Operator.iri(input['resultSeverity'], **options) if input['resultSeverity']
134
+ result.component = Algebra::Operator.iri(input['sourceConstraintComponent'], **options) if input['sourceConstraintComponent']
135
+ result.shape = Algebra::Operator.iri(input['sourceShape'], **options) if input['sourceShape']
136
+ result.value = Algebra::Operator.to_rdf(:value, input['value'], **options) if input['value']
137
+ result.details = Algebra::Operator.to_rdf(:details, input['details'], **options) if input['details']
138
+ result.message = Algebra::Operator.to_rdf(:message, input['message'], **options) if input['message']
139
+ result
140
+ end
138
141
  end
139
142
 
140
143
  # To results are eql? if their overlapping properties are equal
data/lib/shacl.rb CHANGED
@@ -21,8 +21,8 @@ module SHACL
21
21
  # @option (see Shapes#from_graph)
22
22
  # @return (see Shapes#from_graph)
23
23
  # @raise (see Shapes#from_graph)
24
- def self.get_shapes(shape_graph, **options)
25
- Shapes.from_graph(shape_graph, **options)
24
+ def self.get_shapes(shapes_graph, **options)
25
+ Shapes.from_graph(shapes_graph, **options)
26
26
  end
27
27
 
28
28
  ##
@@ -33,8 +33,13 @@ module SHACL
33
33
  # @return (see Shapes#from_graph)
34
34
  # @raise (see Shapes#from_graph)
35
35
  def self.open(input, **options)
36
- graph = RDF::Graph.load(input, **options)
37
- self.get_shapes(graph, loaded_graphs: [RDF::URI(input, canonicalize: true)], **options)
36
+ # Create graph backed by repo to allow a graph_name
37
+ graph = RDF::Graph.load(input,
38
+ graph_name: RDF::URI(input),
39
+ data: RDF::Repository.new)
40
+ self.get_shapes(graph,
41
+ loaded_graphs: [RDF::URI(input, canonicalize: true)],
42
+ **options)
38
43
  end
39
44
 
40
45
  ##
@@ -77,7 +82,7 @@ module SHACL
77
82
  attr_reader :code
78
83
 
79
84
  ##
80
- # Initializes a new patch error instance.
85
+ # Initializes a new error instance.
81
86
  #
82
87
  # @param [String, #to_s] message
83
88
  # @param [Hash{Symbol => Object}] options