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