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.
- checksums.yaml +4 -4
- data/README.md +7 -2
- data/VERSION +1 -1
- data/lib/rdf/vocab/shacl.rb +1096 -1096
- data/lib/shacl/algebra/and.rb +3 -2
- data/lib/shacl/algebra/constraint_component.rb +9 -0
- data/lib/shacl/algebra/node_shape.rb +10 -16
- data/lib/shacl/algebra/not.rb +3 -2
- data/lib/shacl/algebra/operator.rb +276 -44
- data/lib/shacl/algebra/or.rb +3 -2
- data/lib/shacl/algebra/pattern.rb +43 -0
- data/lib/shacl/algebra/property_shape.rb +11 -16
- data/lib/shacl/algebra/{qualified_value_shape.rb → qualified_value.rb} +19 -5
- data/lib/shacl/algebra/shape.rb +12 -41
- data/lib/shacl/algebra/sparql_constraint.rb +160 -0
- data/lib/shacl/algebra/xone.rb +3 -2
- data/lib/shacl/algebra.rb +20 -12
- data/lib/shacl/context.rb +5 -0
- data/lib/shacl/shapes.rb +40 -11
- data/lib/shacl/validation_result.rb +22 -19
- data/lib/shacl.rb +10 -5
- metadata +32 -3
data/lib/shacl/algebra/shape.rb
CHANGED
@@ -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(:<,
|
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(:<=,
|
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(:>,
|
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
|
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(:>=,
|
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
|
data/lib/shacl/algebra/xone.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module SHACL::Algebra
|
2
2
|
##
|
3
|
-
class
|
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 :
|
9
|
-
autoload :
|
10
|
-
autoload :
|
11
|
-
autoload :
|
12
|
-
autoload :
|
13
|
-
autoload :
|
14
|
-
autoload :
|
15
|
-
autoload :
|
16
|
-
autoload :
|
17
|
-
autoload :
|
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
|
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
|
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
|
42
|
-
|
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
|
-
|
69
|
-
graph = RDF::Graph.new do |g|
|
70
|
-
|
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:
|
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.
|
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"
|
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"
|
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"
|
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
|
-
#
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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(
|
25
|
-
Shapes.from_graph(
|
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
|
37
|
-
|
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
|
85
|
+
# Initializes a new error instance.
|
81
86
|
#
|
82
87
|
# @param [String, #to_s] message
|
83
88
|
# @param [Hash{Symbol => Object}] options
|