shacl 0.1.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 +7 -0
- data/LICENSE +24 -0
- data/README.md +159 -0
- data/VERSION +1 -0
- data/etc/doap.ttl +35 -0
- data/lib/rdf/vocab/shacl.rb +2073 -0
- data/lib/shacl.rb +93 -0
- data/lib/shacl/algebra.rb +34 -0
- data/lib/shacl/algebra/and.rb +51 -0
- data/lib/shacl/algebra/node_shape.rb +80 -0
- data/lib/shacl/algebra/not.rb +30 -0
- data/lib/shacl/algebra/operator.rb +329 -0
- data/lib/shacl/algebra/or.rb +46 -0
- data/lib/shacl/algebra/property_shape.rb +190 -0
- data/lib/shacl/algebra/qualified_value_shape.rb +47 -0
- data/lib/shacl/algebra/shape.rb +499 -0
- data/lib/shacl/algebra/xone.rb +65 -0
- data/lib/shacl/context.rb +41 -0
- data/lib/shacl/format.rb +88 -0
- data/lib/shacl/refinements.rb +198 -0
- data/lib/shacl/shapes.rb +160 -0
- data/lib/shacl/validation_report.rb +109 -0
- data/lib/shacl/validation_result.rb +153 -0
- data/lib/shacl/version.rb +19 -0
- metadata +217 -0
data/lib/shacl.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'shacl/format'
|
2
|
+
require 'shacl/shapes'
|
3
|
+
require 'shacl/refinements'
|
4
|
+
require 'rdf/vocab/shacl'
|
5
|
+
|
6
|
+
##
|
7
|
+
# A SHACL runtime for RDF.rb.
|
8
|
+
#
|
9
|
+
# @see https://www.w3.org/TR/shacl/
|
10
|
+
|
11
|
+
module SHACL
|
12
|
+
autoload :Algebra, 'shacl/algebra'
|
13
|
+
autoload :VERSION, 'shacl/version'
|
14
|
+
|
15
|
+
##
|
16
|
+
# Transform the given _Shapes Graph_ into a set of executable shapes.
|
17
|
+
#
|
18
|
+
# A _Shapes Graph_ may contain an `owl:imports` property referencing additional _Shapes Graphs_, which are resolved until no more imports are found.
|
19
|
+
#
|
20
|
+
# @param (see Shapes#from_graph)
|
21
|
+
# @option (see Shapes#from_graph)
|
22
|
+
# @return (see Shapes#from_graph)
|
23
|
+
# @raise (see Shapes#from_graph)
|
24
|
+
def self.get_shapes(shape_graph, **options)
|
25
|
+
Shapes.from_graph(shape_graph, **options)
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Parse a given resource into a _Shapes Graph_.
|
30
|
+
#
|
31
|
+
# @param [String, IO, StringIO, #to_s] input
|
32
|
+
# @option (see Shapes#from_graph)
|
33
|
+
# @return (see Shapes#from_graph)
|
34
|
+
# @raise (see Shapes#from_graph)
|
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)
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Retrieve shapes from a sh:shapesGraph reference within queryable
|
42
|
+
#
|
43
|
+
# @param (see Shapes#from_queryable)
|
44
|
+
# @option (see Shapes#from_queryable)
|
45
|
+
# @return (see Shapes#from_queryable)
|
46
|
+
# @raise (see Shapes#from_queryable)
|
47
|
+
def self.from_queryable(queryable, **options)
|
48
|
+
Shapes.from_queryable(queryable, **options)
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# The _Shapes Graph_, is established similar to the _Data Graph_, but may be `nil`. If `nil`, the _Data Graph_ may reference a _Shapes Graph_ thorugh an `sh:shapesGraph` property.
|
53
|
+
#
|
54
|
+
# Additionally, a _Shapes Graph_ may contain an `owl:imports` property referencing additional _Shapes Graphs_, which are resolved until no more imports are found.
|
55
|
+
#
|
56
|
+
# Load and validate the given SHACL `expression` string against `queriable`.
|
57
|
+
#
|
58
|
+
# @param [String, IO, StringIO, #to_s] input
|
59
|
+
# @param [RDF::Queryable] queryable
|
60
|
+
# @param [Hash{Symbol => Object}] options
|
61
|
+
# @options (see Shapes#initialize)
|
62
|
+
# @return (see Shapes#execute)
|
63
|
+
# @raise (see Shapes#execute)
|
64
|
+
def self.execute(input, queryable = nil, **options)
|
65
|
+
queryable = queryable || RDF::Graph.new
|
66
|
+
shapes = if input
|
67
|
+
self.open(input, **options)
|
68
|
+
else
|
69
|
+
Shapes.from_queryable(queryable)
|
70
|
+
end
|
71
|
+
|
72
|
+
shapes.execute(queryable, **options)
|
73
|
+
end
|
74
|
+
|
75
|
+
class Error < StandardError
|
76
|
+
# The status code associated with this error
|
77
|
+
attr_reader :code
|
78
|
+
|
79
|
+
##
|
80
|
+
# Initializes a new patch error instance.
|
81
|
+
#
|
82
|
+
# @param [String, #to_s] message
|
83
|
+
# @param [Hash{Symbol => Object}] options
|
84
|
+
# @option options [Integer] :code (422)
|
85
|
+
def initialize(message, **options)
|
86
|
+
@code = options.fetch(:status_code, 422)
|
87
|
+
super(message.to_s)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Shape expectation not satisfied
|
92
|
+
class StructureError < Error; end
|
93
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
$:.unshift(File.expand_path("../..", __FILE__))
|
2
|
+
require 'sxp'
|
3
|
+
require_relative "algebra/operator"
|
4
|
+
|
5
|
+
module SHACL
|
6
|
+
# Based on the SPARQL Algebra, operators for executing a patch
|
7
|
+
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'
|
18
|
+
|
19
|
+
def self.from_json(operator, **options)
|
20
|
+
raise ArgumentError, "from_json: operator not a Hash: #{operator.inspect}" unless operator.is_a?(Hash)
|
21
|
+
type = operator.fetch('type', [])
|
22
|
+
type << (operator["path"] ? 'PropertyShape' : 'NodeShape') if type.empty?
|
23
|
+
klass = case
|
24
|
+
when type.include?('NodeShape') then NodeShape
|
25
|
+
when type.include?('PropertyShape') then PropertyShape
|
26
|
+
else raise ArgumentError, "from_json: unknown type #{type.inspect}"
|
27
|
+
end
|
28
|
+
|
29
|
+
klass.from_json(operator, **options)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module SHACL::Algebra
|
2
|
+
##
|
3
|
+
class And < Operator
|
4
|
+
NAME = :and
|
5
|
+
|
6
|
+
##
|
7
|
+
# Specifies the condition that each value node conforms to all provided shapes. This is comparable to conjunction and the logical "and" operator.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# ex:SuperShape
|
11
|
+
# a sh:NodeShape ;
|
12
|
+
# sh:property [
|
13
|
+
# sh:path ex:property ;
|
14
|
+
# sh:minCount 1 ;
|
15
|
+
# ] .
|
16
|
+
#
|
17
|
+
# ex:ExampleAndShape
|
18
|
+
# a sh:NodeShape ;
|
19
|
+
# sh:targetNode ex:ValidInstance, ex:InvalidInstance ;
|
20
|
+
# sh:and (
|
21
|
+
# ex:SuperShape
|
22
|
+
# [
|
23
|
+
# sh:path ex:property ;
|
24
|
+
# sh:maxCount 1 ;
|
25
|
+
# ]
|
26
|
+
# ) .
|
27
|
+
#
|
28
|
+
# @param [RDF::Term] node
|
29
|
+
# @param [Hash{Symbol => Object}] options
|
30
|
+
# @return [Array<SHACL::ValidationResult>]
|
31
|
+
def conforms(node, path: nil, depth: 0, **options)
|
32
|
+
log_debug(NAME, depth: depth) {SXP::Generator.string({node: node}.to_sxp_bin)}
|
33
|
+
operands.each do |op|
|
34
|
+
results = op.conforms(node, depth: depth + 1, **options)
|
35
|
+
if !results.all?(&:conform?)
|
36
|
+
return not_satisfied(focus: node, path: path,
|
37
|
+
value: node,
|
38
|
+
message: "node does not conform to all shapes",
|
39
|
+
resultSeverity: options.fetch(:severity),
|
40
|
+
component: RDF::Vocab::SHACL.AndConstraintComponent,
|
41
|
+
depth: depth, **options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
satisfy(focus: node, path: path,
|
45
|
+
value: node,
|
46
|
+
message: "node conforms to all shapes",
|
47
|
+
component: RDF::Vocab::SHACL.AndConstraintComponent,
|
48
|
+
depth: depth, **options)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require_relative "shape"
|
2
|
+
|
3
|
+
module SHACL::Algebra
|
4
|
+
##
|
5
|
+
class NodeShape < SHACL::Algebra::Shape
|
6
|
+
NAME = :NodeShape
|
7
|
+
|
8
|
+
# Validates the specified `node` within `graph`, a list of {ValidationResult}.
|
9
|
+
#
|
10
|
+
# A node conforms if it is not deactivated and all of its operands conform.
|
11
|
+
#
|
12
|
+
# @param [RDF::Term] node
|
13
|
+
# @param [Hash{Symbol => Object}] options
|
14
|
+
# @return [Array<SHACL::ValidationResult>]
|
15
|
+
# Returns one or more validation results for each operand.
|
16
|
+
def conforms(node, depth: 0, **options)
|
17
|
+
return [] if deactivated?
|
18
|
+
options = id ? options.merge(shape: id) : options
|
19
|
+
options = options.merge(severity: RDF::Vocab::SHACL.Violation)
|
20
|
+
log_debug(NAME, depth: depth) {SXP::Generator.string({id: id, node: node}.to_sxp_bin)}
|
21
|
+
|
22
|
+
# Add some instance options to the argument
|
23
|
+
options = %i{
|
24
|
+
flags
|
25
|
+
qualifiedMinCount
|
26
|
+
qualifiedMaxCount
|
27
|
+
qualifiedValueShapesDisjoint
|
28
|
+
severity
|
29
|
+
}.inject(options) do |memo, sym|
|
30
|
+
@options[sym] ? memo.merge(sym => @options[sym]) : memo
|
31
|
+
end
|
32
|
+
|
33
|
+
# Evaluate against builtins
|
34
|
+
builtin_results = @options.map do |k, v|
|
35
|
+
self.send("builtin_#{k}".to_sym, v, node, nil, [node], depth: depth + 1, **options) if self.respond_to?("builtin_#{k}".to_sym)
|
36
|
+
end.flatten.compact
|
37
|
+
|
38
|
+
# Handle closed shapes
|
39
|
+
# FIXME: this only considers URI paths, not property paths
|
40
|
+
closed_results = []
|
41
|
+
if @options[:closed]
|
42
|
+
shape_paths = operands.select {|o| o.is_a?(PropertyShape)}.map(&:path)
|
43
|
+
shape_properties = shape_paths.select {|p| p.is_a?(RDF::URI)}
|
44
|
+
shape_properties += Array(@options[:ignoredProperties])
|
45
|
+
|
46
|
+
closed_results = graph.query(subject: node).map do |statement|
|
47
|
+
next if shape_properties.include?(statement.predicate)
|
48
|
+
not_satisfied(focus: node,
|
49
|
+
value: statement.object,
|
50
|
+
path: statement.predicate,
|
51
|
+
message: "closed node has extra property",
|
52
|
+
resultSeverity: options.fetch(:severity),
|
53
|
+
component: RDF::Vocab::SHACL.ClosedConstraintComponent,
|
54
|
+
**options)
|
55
|
+
end.compact
|
56
|
+
end
|
57
|
+
|
58
|
+
# Evaluate against operands
|
59
|
+
op_results = operands.map do |op|
|
60
|
+
res = op.conforms(node,
|
61
|
+
focus: options.fetch(:focusNode, node),
|
62
|
+
depth: depth + 1,
|
63
|
+
**options)
|
64
|
+
if op.is_a?(NodeShape) && !res.all?(&:conform?)
|
65
|
+
# Special case for embedded NodeShape
|
66
|
+
not_satisfied(focus: node,
|
67
|
+
value: node,
|
68
|
+
message: "node does not conform to #{op.id}",
|
69
|
+
resultSeverity: options.fetch(:severity),
|
70
|
+
component: RDF::Vocab::SHACL.NodeConstraintComponent,
|
71
|
+
**options)
|
72
|
+
else
|
73
|
+
res
|
74
|
+
end
|
75
|
+
end.flatten.compact
|
76
|
+
|
77
|
+
builtin_results + closed_results + op_results
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module SHACL::Algebra
|
2
|
+
##
|
3
|
+
class Not < Operator
|
4
|
+
NAME = :not
|
5
|
+
|
6
|
+
##
|
7
|
+
# Specifies the condition that each value node cannot conform to a given shape. This is comparable to negation and the logical "not" operator.
|
8
|
+
#
|
9
|
+
# @param [RDF::Term] node
|
10
|
+
# @param [Hash{Symbol => Object}] options
|
11
|
+
# @return [Array<SHACL::ValidationResult>]
|
12
|
+
def conforms(node, path: nil, depth: 0, **options)
|
13
|
+
log_debug(NAME, depth: depth) {SXP::Generator.string({node: node}.to_sxp_bin)}
|
14
|
+
operands.each do |op|
|
15
|
+
results = op.conforms(node, depth: depth + 1, **options)
|
16
|
+
if results.any?(&:conform?)
|
17
|
+
return not_satisfied(focus: node, path: path,
|
18
|
+
message: "node does not conform to some shape",
|
19
|
+
resultSeverity: options.fetch(:severity),
|
20
|
+
component: RDF::Vocab::SHACL.NotConstraintComponent,
|
21
|
+
value: node, depth: depth, **options)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
satisfy(focus: node, path: path,
|
25
|
+
message: "node conforms to all shapes",
|
26
|
+
component: RDF::Vocab::SHACL.NotConstraintComponent,
|
27
|
+
value: node, depth: depth, **options)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,329 @@
|
|
1
|
+
require 'sparql/algebra'
|
2
|
+
require 'shacl/validation_result'
|
3
|
+
require 'json/ld'
|
4
|
+
|
5
|
+
module SHACL::Algebra
|
6
|
+
|
7
|
+
##
|
8
|
+
# The SHACL operator.
|
9
|
+
#
|
10
|
+
# @abstract
|
11
|
+
class Operator < SPARQL::Algebra::Operator
|
12
|
+
include RDF::Util::Logger
|
13
|
+
extend JSON::LD::Utils
|
14
|
+
|
15
|
+
# All keys associated with shapes which are set in options
|
16
|
+
#
|
17
|
+
# @return [Array<Symbol>]
|
18
|
+
ALL_KEYS = %i(
|
19
|
+
id type label name comment description deactivated severity
|
20
|
+
order group defaultValue path
|
21
|
+
targetNode targetClass targetSubjectsOf targetObjectsOf
|
22
|
+
class datatype nodeKind
|
23
|
+
minCount maxCount
|
24
|
+
minExclusive minInclusive maxExclusive maxInclusive
|
25
|
+
minLength maxLength
|
26
|
+
pattern flags languageIn uniqueLang
|
27
|
+
qualifiedValueShapesDisjoint qualifiedMinCount qualifiedMaxCount
|
28
|
+
equals disjoint lessThan lessThanOrEquals
|
29
|
+
closed ignoredProperties hasValue in
|
30
|
+
).freeze
|
31
|
+
|
32
|
+
# Initialization options
|
33
|
+
attr_accessor :options
|
34
|
+
|
35
|
+
# Graph against which shapes are validaed
|
36
|
+
attr_accessor :graph
|
37
|
+
|
38
|
+
## Class methods
|
39
|
+
class << self
|
40
|
+
##
|
41
|
+
# Creates an operator instance from a parsed SHACL representation
|
42
|
+
# @param [Hash] operator
|
43
|
+
# @param [Hash] options ({})
|
44
|
+
# @option options [Hash{String => RDF::URI}] :prefixes
|
45
|
+
# @return [Operator]
|
46
|
+
def from_json(operator, **options)
|
47
|
+
operands = []
|
48
|
+
node_opts = options.dup
|
49
|
+
operator.each do |k, v|
|
50
|
+
next if v.nil?
|
51
|
+
case k
|
52
|
+
# List properties
|
53
|
+
when 'and'
|
54
|
+
elements = as_array(v).map {|vv| SHACL::Algebra.from_json(vv, **options)}
|
55
|
+
operands << And.new(*elements, **options.dup)
|
56
|
+
when 'class' then node_opts[:class] = as_array(v).map {|vv| iri(vv, **options)} if v
|
57
|
+
when 'datatype' then node_opts[:datatype] = iri(v, **options)
|
58
|
+
when 'disjoint' then node_opts[:disjoint] = as_array(v).map {|vv| iri(vv, **options)} if v
|
59
|
+
when 'equals' then node_opts[:equals] = iri(v, **options)
|
60
|
+
when 'id' then node_opts[:id] = iri(v, vocab: false, **options)
|
61
|
+
when 'ignoredProperties' then node_opts[:ignoredProperties] = as_array(v).map {|vv| iri(vv, **options)} if v
|
62
|
+
when 'lessThan' then node_opts[:lessThan] = iri(v, **options)
|
63
|
+
when 'lessThanOrEquals' then node_opts[:lessThanOrEquals] = iri(v, **options)
|
64
|
+
when 'node'
|
65
|
+
operands.push(*as_array(v).map {|vv| NodeShape.from_json(vv, **options)})
|
66
|
+
when 'nodeKind' then node_opts[:nodeKind] = iri(v, **options)
|
67
|
+
when 'not'
|
68
|
+
elements = as_array(v).map {|vv| SHACL::Algebra.from_json(vv, **options)}
|
69
|
+
operands << Not.new(*elements, **options.dup)
|
70
|
+
when 'or'
|
71
|
+
elements = as_array(v).map {|vv| SHACL::Algebra.from_json(vv, **options)}
|
72
|
+
operands << Or.new(*elements, **options.dup)
|
73
|
+
when 'path' then node_opts[:path] = parse_path(v, **options)
|
74
|
+
when 'property'
|
75
|
+
operands.push(*as_array(v).map {|vv| PropertyShape.from_json(vv, **options)})
|
76
|
+
when 'qualifiedValueShape'
|
77
|
+
elements = as_array(v).map {|vv| SHACL::Algebra.from_json(vv, **options)}
|
78
|
+
operands << QualifiedValueShape.new(*elements, **options.dup)
|
79
|
+
when 'severity' then node_opts[:severity] = iri(v, **options)
|
80
|
+
when 'targetClass' then node_opts[:targetClass] = as_array(v).map {|vv| iri(vv, **options)} if v
|
81
|
+
when 'targetNode'
|
82
|
+
node_opts[:targetNode] = as_array(v).map do |vv|
|
83
|
+
from_expanded_value(vv, **options)
|
84
|
+
end if v
|
85
|
+
when 'targetObjectsOf' then node_opts[:targetObjectsOf] = as_array(v).map {|vv| iri(vv, **options)} if v
|
86
|
+
when 'targetSubjectsOf' then node_opts[:targetSubjectsOf] = as_array(v).map {|vv| iri(vv, **options)} if v
|
87
|
+
when 'type' then node_opts[:type] = as_array(v).map {|vv| iri(vv, **options)} if v
|
88
|
+
when 'xone'
|
89
|
+
elements = as_array(v).map {|vv| SHACL::Algebra.from_json(vv, **options)}
|
90
|
+
operands << Xone.new(*elements, **options.dup)
|
91
|
+
else
|
92
|
+
# Add as a plain option if it is recognized
|
93
|
+
node_opts[k.to_sym] = to_rdf(k.to_sym, v, **options) if ALL_KEYS.include?(k.to_sym)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
new(*operands, **node_opts)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Create URIs
|
101
|
+
# @param (see #iri)
|
102
|
+
# @option (see #iri)
|
103
|
+
# @return (see #iri)
|
104
|
+
def iri(value, base: RDF::Vocab::SHACL.to_uri, vocab: true, **options)
|
105
|
+
# Context will have been pre-loaded
|
106
|
+
@context ||= JSON::LD::Context.parse("http://github.com/ruby-rdf/shacl/")
|
107
|
+
|
108
|
+
value = value['id'] || value['@id'] if value.is_a?(Hash)
|
109
|
+
result = @context.expand_iri(value, base: base, vocab: vocab)
|
110
|
+
result = RDF::URI(result) if result.is_a?(String)
|
111
|
+
if result.respond_to?(:qname) && result.qname
|
112
|
+
result = RDF::URI.new(result.to_s) if result.frozen?
|
113
|
+
result.lexical = result.qname.join(':')
|
114
|
+
end
|
115
|
+
result
|
116
|
+
end
|
117
|
+
|
118
|
+
# Turn a JSON-LD value into its RDF representation
|
119
|
+
# @see JSON::LD::ToRDF.item_to_rdf
|
120
|
+
# @param [Symbol] term
|
121
|
+
# @param [Object] item
|
122
|
+
# @return RDF::Term
|
123
|
+
def to_rdf(term, item, **options)
|
124
|
+
@context ||= JSON::LD::Context.parse("http://github.com/ruby-rdf/shacl/")
|
125
|
+
|
126
|
+
return item.map {|v| to_rdf(term, v, **options)} if item.is_a?(Array)
|
127
|
+
|
128
|
+
case
|
129
|
+
when item.is_a?(TrueClass) || item.is_a?(FalseClass) || item.is_a?(Numeric)
|
130
|
+
return RDF::Literal(item)
|
131
|
+
when value?(item)
|
132
|
+
value, datatype = item.fetch('@value'), item.fetch('type', nil)
|
133
|
+
case value
|
134
|
+
when TrueClass, FalseClass, Numeric
|
135
|
+
return RDF::Literal(value)
|
136
|
+
else
|
137
|
+
datatype ||= item.has_key?('@direction') ?
|
138
|
+
RDF::URI("https://www.w3.org/ns/i18n##{item.fetch('@language', '').downcase}_#{item['@direction']}") :
|
139
|
+
(item.has_key?('@language') ? RDF.langString : RDF::XSD.string)
|
140
|
+
end
|
141
|
+
datatype = iri(datatype) if datatype
|
142
|
+
|
143
|
+
# Initialize literal as an RDF literal using value and datatype. If element has the key @language and datatype is xsd:string, then add the value associated with the @language key as the language of the object.
|
144
|
+
language = item.fetch('@language', nil) if datatype == RDF.langString
|
145
|
+
return RDF::Literal.new(value, datatype: datatype, language: language)
|
146
|
+
when node?(item)
|
147
|
+
return iri(item, **options)
|
148
|
+
when list?(item)
|
149
|
+
RDF::List(*item['@list'].map {|v| to_rdf(term, v, **options)})
|
150
|
+
when item.is_a?(String)
|
151
|
+
RDF::Literal(item)
|
152
|
+
else
|
153
|
+
raise "Can't transform #{item.inspect} to RDF on property #{term}"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Interpret a JSON-LD expanded value
|
158
|
+
# @param [Hash] item
|
159
|
+
# @return [RDF::Term]
|
160
|
+
def from_expanded_value(item, **options)
|
161
|
+
if item['@value']
|
162
|
+
value, datatype = item.fetch('@value'), item.fetch('type', nil)
|
163
|
+
case value
|
164
|
+
when TrueClass, FalseClass
|
165
|
+
value = value.to_s
|
166
|
+
datatype ||= RDF::XSD.boolean.to_s
|
167
|
+
when Numeric
|
168
|
+
# Don't serialize as double if there are no fractional bits
|
169
|
+
as_double = value.ceil != value || value >= 1e21 || datatype == RDF::XSD.double
|
170
|
+
lit = if as_double
|
171
|
+
RDF::Literal::Double.new(value, canonicalize: true)
|
172
|
+
else
|
173
|
+
RDF::Literal.new(value.numerator, canonicalize: true)
|
174
|
+
end
|
175
|
+
|
176
|
+
datatype ||= lit.datatype
|
177
|
+
value = lit.to_s.sub("E+", "E")
|
178
|
+
else
|
179
|
+
datatype ||= item.has_key?('@language') ? RDF.langString : RDF::XSD.string
|
180
|
+
end
|
181
|
+
datatype = iri(datatype) if datatype
|
182
|
+
language = item.fetch('@language', nil) if datatype == RDF.langString
|
183
|
+
RDF::Literal.new(value, datatype: datatype, language: language)
|
184
|
+
elsif item['id']
|
185
|
+
self.iri(item['id'], **options)
|
186
|
+
else
|
187
|
+
RDF::Node.new
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
##
|
192
|
+
# Parse the "patH" attribute into a SPARQL Property Path and evaluate to find related nodes.
|
193
|
+
#
|
194
|
+
# @param [Object] path
|
195
|
+
# @return [RDF::URI, SPARQL::Algebra::Expression]
|
196
|
+
def parse_path(path, **options)
|
197
|
+
case path
|
198
|
+
when RDF::URI then path
|
199
|
+
when String then iri(path)
|
200
|
+
when Hash
|
201
|
+
# Creates a SPARQL S-Expression resulting in a query which can be used to find corresponding
|
202
|
+
{
|
203
|
+
alternativePath: :alt,
|
204
|
+
inversePath: :reverse,
|
205
|
+
oneOrMorePath: :"path+",
|
206
|
+
"@list": :seq,
|
207
|
+
zeroOrMorePath: :"path*",
|
208
|
+
zeroOrOnePath: :"path?",
|
209
|
+
}.each do |prop, op_sym|
|
210
|
+
if path[prop.to_s]
|
211
|
+
value = path[prop.to_s]
|
212
|
+
value = value['@list'] if value.is_a?(Hash) && value.key?('@list')
|
213
|
+
value = [value] if !value.is_a?(Array)
|
214
|
+
value = value.map {|e| parse_path(e, **options)}
|
215
|
+
op = SPARQL::Algebra::Operator.for(op_sym)
|
216
|
+
if value.length > op.arity
|
217
|
+
# Divide into the first operand followed by the operator re-applied to the reamining operands
|
218
|
+
value = value.first, apply_op(op, value[1..-1])
|
219
|
+
end
|
220
|
+
return op.new(*value)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
if path['id']
|
225
|
+
iri(path['id'])
|
226
|
+
else
|
227
|
+
log_error('PropertyPath', "Can't handle path", **options) {path.to_sxp}
|
228
|
+
end
|
229
|
+
else
|
230
|
+
log_error('PropertyPath', "Can't handle path", **options) {path.to_sxp}
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Recursively apply operand to sucessive values until the argument count which is expected is achieved
|
235
|
+
def apply_op(op, values)
|
236
|
+
if values.length > op.arity
|
237
|
+
values = values.first, apply_op(op, values[1..-1])
|
238
|
+
end
|
239
|
+
op.new(*values)
|
240
|
+
end
|
241
|
+
protected :apply_op
|
242
|
+
end
|
243
|
+
|
244
|
+
# The ID of this operator
|
245
|
+
# @return [RDF::Resource]
|
246
|
+
def id; @options[:id]; end
|
247
|
+
|
248
|
+
# The types associated with this operator
|
249
|
+
# @return [Array<RDF::URI>]
|
250
|
+
def type; @options[:type]; end
|
251
|
+
|
252
|
+
# Any label associated with this operator
|
253
|
+
# @return [RDF::Literal]
|
254
|
+
def label; @options[:label]; end
|
255
|
+
|
256
|
+
# Is this shape deactivated?
|
257
|
+
# @return [Boolean]
|
258
|
+
def deactivated?; @options[:deactivated] == RDF::Literal::TRUE; end
|
259
|
+
|
260
|
+
# Any comment associated with this operator
|
261
|
+
# @return [RDF::Literal]
|
262
|
+
def comment; @options[:comment]; end
|
263
|
+
|
264
|
+
# Create URIs
|
265
|
+
# @param [RDF::Value, String] value
|
266
|
+
# @param [RDF::URI] base Base IRI used for resolving relative values (RDF::Vocab::SHACL.to_uri).
|
267
|
+
# @param [Boolean] vocab resolve vocabulary relative to the builtin context.
|
268
|
+
# @param [Hash{Symbol => Object}] options
|
269
|
+
# @return [RDF::Value]
|
270
|
+
def iri(value, base: RDF::Vocab::SHACL.to_uri, vocab: true, **options)
|
271
|
+
self.class.iri(value, base: base, vocab: vocab, **options)
|
272
|
+
end
|
273
|
+
|
274
|
+
# Validates the specified `node` within `graph`, a list of {ValidationResult}.
|
275
|
+
#
|
276
|
+
# A node conforms if it is not deactivated and all of its operands conform.
|
277
|
+
#
|
278
|
+
# @param [RDF::Term] node
|
279
|
+
# @param [Hash{Symbol => Object}] options
|
280
|
+
# @return [Array<ValidationResult>]
|
281
|
+
def conforms(node, depth: 0, **options)
|
282
|
+
raise NotImplemented
|
283
|
+
end
|
284
|
+
|
285
|
+
def to_sxp_bin
|
286
|
+
expressions = ALL_KEYS.inject([self.class.const_get(:NAME)]) do |memo, sym|
|
287
|
+
@options[sym] ? memo.push([sym, *@options[sym]]) : memo
|
288
|
+
end + operands
|
289
|
+
|
290
|
+
expressions.to_sxp_bin
|
291
|
+
end
|
292
|
+
|
293
|
+
##
|
294
|
+
# Create a result that satisfies the shape.
|
295
|
+
#
|
296
|
+
# @param [RDF::Term] focus
|
297
|
+
# @param [RDF::Resource] shape
|
298
|
+
# @param [RDF::URI] component
|
299
|
+
# @param [RDF::URI] resultSeverity (nil)
|
300
|
+
# @param [Array<RDF::URI>] path (nil)
|
301
|
+
# @param [RDF::Term] value (nil)
|
302
|
+
# @param [RDF::Term] details (nil)
|
303
|
+
# @param [String] message (nil)
|
304
|
+
# @return [Array<SHACL::ValidationResult>]
|
305
|
+
def satisfy(focus:, shape:, component:, resultSeverity: nil, path: nil, value: nil, details: nil, message: nil, **options)
|
306
|
+
log_debug(self.class.const_get(:NAME), "#{'not ' if resultSeverity}satisfied #{value.to_sxp if value}#{': ' + message if message}", **options)
|
307
|
+
[SHACL::ValidationResult.new(focus, path, shape, resultSeverity, component,
|
308
|
+
details, value, message)]
|
309
|
+
end
|
310
|
+
|
311
|
+
##
|
312
|
+
# Create a result that does not satisfies the shape.
|
313
|
+
#
|
314
|
+
# @param [RDF::Term] focus
|
315
|
+
# @param [RDF::Resource] shape
|
316
|
+
# @param [RDF::URI] component
|
317
|
+
# @param [RDF::URI] resultSeverity (RDF:::Vocab::SHACL.Violation)
|
318
|
+
# @param [Array<RDF::URI>] path (nil)
|
319
|
+
# @param [RDF::Term] value (nil)
|
320
|
+
# @param [RDF::Term] details (nil)
|
321
|
+
# @param [String] message (nil)
|
322
|
+
# @return [Array<SHACL::ValidationResult>]
|
323
|
+
def not_satisfied(focus:, shape:, component:, resultSeverity: RDF::Vocab::SHACL.Violation, path: nil, value: nil, details: nil, message: nil, **options)
|
324
|
+
log_info(self.class.const_get(:NAME), "not satisfied #{value.to_sxp if value}#{': ' + message if message}", **options)
|
325
|
+
[SHACL::ValidationResult.new(focus, path, shape, resultSeverity, component,
|
326
|
+
details, value, message)]
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|