shacl 0.1.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
 
@@ -102,8 +125,12 @@ module SHACL
102
125
  [:shapes, super]
103
126
  end
104
127
 
105
- def to_sxp
106
- to_sxp_bin.to_sxp
128
+ ##
129
+ # Transform Shapes into an SXP.
130
+ #
131
+ # @return [String]
132
+ def to_sxp(**options)
133
+ to_sxp_bin.to_sxp(**options)
107
134
  end
108
135
 
109
136
  SHAPES_FRAME = JSON.parse(%({
@@ -111,11 +138,12 @@ module SHACL
111
138
  "id": "@id",
112
139
  "type": {"@id": "@type", "@container": "@set"},
113
140
  "@vocab": "http://www.w3.org/ns/shacl#",
141
+ "owl": "http://www.w3.org/2002/07/owl#",
114
142
  "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
115
143
  "shacl": "http://www.w3.org/ns/shacl#",
116
144
  "sh": "http://www.w3.org/ns/shacl#",
117
145
  "xsd": "http://www.w3.org/2001/XMLSchema#",
118
- "and": {"@type": "@id", "@container": "@list"},
146
+ "and": {"@type": "@id"},
119
147
  "annotationProperty": {"@type": "@id"},
120
148
  "class": {"@type": "@id"},
121
149
  "comment": "http://www.w3.org/2000/01/rdf-schema#comment",
@@ -126,30 +154,35 @@ module SHACL
126
154
  "entailment": {"@type": "@id"},
127
155
  "equals": {"@type": "@id"},
128
156
  "ignoredProperties": {"@type": "@id", "@container": "@list"},
157
+ "imports": {"@id": "owl:imports", "@type": "@id"},
129
158
  "in": {"@type": "@none", "@container": "@list"},
130
159
  "inversePath": {"@type": "@id"},
131
160
  "label": "http://www.w3.org/2000/01/rdf-schema#label",
132
161
  "languageIn": {"@container": "@list"},
133
162
  "lessThan": {"@type": "@id"},
134
163
  "lessThanOrEquals": {"@type": "@id"},
164
+ "namespace": {"@type": "xsd:anyURI"},
135
165
  "nodeKind": {"@type": "@vocab"},
136
- "or": {"@type": "@id", "@container": "@list"},
166
+ "or": {"@type": "@id"},
137
167
  "path": {"@type": "@none"},
168
+ "prefixes": {"@type": "@id"},
138
169
  "property": {"@type": "@id"},
139
170
  "severity": {"@type": "@vocab"},
171
+ "sparql": {"@type": "@id"},
140
172
  "targetClass": {"@type": "@id"},
141
173
  "targetNode": {"@type": "@none"},
142
- "xone": {"@type": "@id", "@container": "@list"}
174
+ "xone": {"@type": "@id"}
143
175
  },
144
176
  "and": {},
145
177
  "class": {},
146
178
  "datatype": {},
147
- "in": {},
179
+ "in": {"@embed": "@never"},
148
180
  "node": {},
149
181
  "nodeKind": {},
150
182
  "not": {},
151
183
  "or": {},
152
184
  "property": {},
185
+ "sparql": {},
153
186
  "targetClass": {},
154
187
  "targetNode": {},
155
188
  "targetObjectsOf": {},
@@ -59,8 +59,12 @@ module SHACL
59
59
  [:ValidationReport, conform?, results].to_sxp_bin
60
60
  end
61
61
 
62
- def to_sxp
63
- self.to_sxp_bin.to_sxp
62
+ ##
63
+ # Transform Report to SXP
64
+ #
65
+ # @return [String]
66
+ def to_sxp(**options)
67
+ self.to_sxp_bin.to_sxp(**options)
64
68
  end
65
69
 
66
70
  def to_s
@@ -50,8 +50,12 @@ module SHACL
50
50
  end.to_sxp_bin
51
51
  end
52
52
 
53
- def to_sxp
54
- self.to_sxp_bin.to_sxp
53
+ ##
54
+ # Transform ValidationResult to SXP
55
+ #
56
+ # @return [String]
57
+ def to_sxp(**options)
58
+ self.to_sxp_bin.to_sxp(**options)
55
59
  end
56
60
 
57
61
  ##
@@ -111,26 +115,29 @@ module SHACL
111
115
  block.call(RDF::Statement(subject, RDF::Vocab::SHACL.resultMessage, RDF::Literal(message))) if message
112
116
  end
113
117
 
114
- # Transform a JSON representation of a result, into a native representation
115
- # @param [Hash] input
116
- # @return [ValidationResult]
117
- def self.from_json(input, **options)
118
- input = JSON.parse(input) if input.is_a?(String)
119
- input = JSON::LD::API.compact(input,
120
- "http://github.com/ruby-rdf/shacl/",
121
- expandContext: "http://github.com/ruby-rdf/shacl/")
122
- raise ArgumentError, "Expect report to be a hash" unless input.is_a?(Hash)
123
- 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
124
130
 
125
- result.focus = Algebra::Operator.to_rdf(:focus, input['focusNode'], base: nil, vocab: false) if input['focusNode']
126
- result.path = Algebra::Operator.parse_path(input['resultPath'], **options) if input['resultPath']
127
- result.resultSeverity = Algebra::Operator.iri(input['resultSeverity'], **options) if input['resultSeverity']
128
- result.component = Algebra::Operator.iri(input['sourceConstraintComponent'], **options) if input['sourceConstraintComponent']
129
- result.shape = Algebra::Operator.iri(input['sourceShape'], **options) if input['sourceShape']
130
- result.value = Algebra::Operator.to_rdf(:value, input['value'], **options) if input['value']
131
- result.details = Algebra::Operator.to_rdf(:details, input['details'], **options) if input['details']
132
- result.message = Algebra::Operator.to_rdf(:message, input['message'], **options) if input['message']
133
- 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
134
141
  end
135
142
 
136
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