shacl 0.1.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +19 -14
- data/VERSION +1 -1
- data/etc/doap.ttl +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 +46 -13
- data/lib/shacl/validation_report.rb +6 -2
- data/lib/shacl/validation_result.rb +28 -21
- data/lib/shacl.rb +10 -5
- metadata +51 -29
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
|
|
@@ -102,8 +125,12 @@ module SHACL
|
|
102
125
|
[:shapes, super]
|
103
126
|
end
|
104
127
|
|
105
|
-
|
106
|
-
|
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"
|
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"
|
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"
|
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
|
-
|
63
|
-
|
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
|
-
|
54
|
-
|
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
|
-
#
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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(
|
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
|