shacl 0.2.1 → 0.3.0

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