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
@@ -0,0 +1,65 @@
|
|
1
|
+
module SHACL::Algebra
|
2
|
+
##
|
3
|
+
class Xone < Operator
|
4
|
+
NAME = :xone
|
5
|
+
|
6
|
+
##
|
7
|
+
# Specifies the condition that each value node conforms to exactly one of the provided shapes.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# ex:XoneConstraintExampleShape
|
11
|
+
# a sh:NodeShape ;
|
12
|
+
# sh:targetClass ex:Person ;
|
13
|
+
# sh:xone (
|
14
|
+
# [
|
15
|
+
# sh:property [
|
16
|
+
# sh:path ex:fullName ;
|
17
|
+
# sh:minCount 1 ;
|
18
|
+
# ]
|
19
|
+
# ]
|
20
|
+
# [
|
21
|
+
# sh:property [
|
22
|
+
# sh:path ex:firstName ;
|
23
|
+
# sh:minCount 1 ;
|
24
|
+
# ] ;
|
25
|
+
# sh:property [
|
26
|
+
# sh:path ex:lastName ;
|
27
|
+
# sh:minCount 1 ;
|
28
|
+
# ]
|
29
|
+
# ]
|
30
|
+
# ) .
|
31
|
+
#
|
32
|
+
# @param [RDF::Term] node
|
33
|
+
# @param [Hash{Symbol => Object}] options
|
34
|
+
# @return [Array<SHACL::ValidationResult>]
|
35
|
+
def conforms(node, path: nil, depth: 0, **options)
|
36
|
+
log_debug(NAME, depth: depth) {SXP::Generator.string({node: node}.to_sxp_bin)}
|
37
|
+
num_conform = operands.inject(0) do |memo, op|
|
38
|
+
results = op.conforms(node, depth: depth + 1, **options)
|
39
|
+
memo += (results.all?(&:conform?) ? 1 : 0)
|
40
|
+
end
|
41
|
+
case num_conform
|
42
|
+
when 0
|
43
|
+
not_satisfied(focus: node, path: path,
|
44
|
+
value: node,
|
45
|
+
message: "node does not conform to any shape",
|
46
|
+
resultSeverity: options.fetch(:severity),
|
47
|
+
component: RDF::Vocab::SHACL.XoneConstraintComponent,
|
48
|
+
depth: depth, **options)
|
49
|
+
when 1
|
50
|
+
satisfy(focus: node, path: path,
|
51
|
+
value: node,
|
52
|
+
message: "node conforms to a single shape",
|
53
|
+
component: RDF::Vocab::SHACL.XoneConstraintComponent,
|
54
|
+
depth: depth, **options)
|
55
|
+
else
|
56
|
+
not_satisfied(focus: node, path: path,
|
57
|
+
value: node,
|
58
|
+
message: "node conforms to #{num_conform} shapes",
|
59
|
+
resultSeverity: options.fetch(:severity),
|
60
|
+
component: RDF::Vocab::SHACL.XoneConstraintComponent,
|
61
|
+
depth: depth, **options)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
# This file generated automatically from http://github.com/ruby-rdf/shacl/
|
4
|
+
require 'json/ld'
|
5
|
+
class JSON::LD::Context
|
6
|
+
add_preloaded("http://github.com/ruby-rdf/shacl/") do
|
7
|
+
new(vocab: "http://www.w3.org/ns/shacl#", processingMode: "json-ld-1.1", term_definitions: {
|
8
|
+
"and" => TermDefinition.new("and", id: "http://www.w3.org/ns/shacl#and", type_mapping: "@id", container_mapping: "@list"),
|
9
|
+
"annotationProperty" => TermDefinition.new("annotationProperty", id: "http://www.w3.org/ns/shacl#annotationProperty", type_mapping: "@id"),
|
10
|
+
"class" => TermDefinition.new("class", id: "http://www.w3.org/ns/shacl#class", type_mapping: "@id"),
|
11
|
+
"comment" => TermDefinition.new("comment", id: "http://www.w3.org/2000/01/rdf-schema#comment", simple: true),
|
12
|
+
"condition" => TermDefinition.new("condition", id: "http://www.w3.org/ns/shacl#condition", type_mapping: "@id"),
|
13
|
+
"datatype" => TermDefinition.new("datatype", id: "http://www.w3.org/ns/shacl#datatype", type_mapping: "@vocab"),
|
14
|
+
"declare" => TermDefinition.new("declare", id: "http://www.w3.org/ns/shacl#declare", type_mapping: "@id"),
|
15
|
+
"disjoint" => TermDefinition.new("disjoint", id: "http://www.w3.org/ns/shacl#disjoint", type_mapping: "@id"),
|
16
|
+
"entailment" => TermDefinition.new("entailment", id: "http://www.w3.org/ns/shacl#entailment", type_mapping: "@id"),
|
17
|
+
"equals" => TermDefinition.new("equals", id: "http://www.w3.org/ns/shacl#equals", type_mapping: "@id"),
|
18
|
+
"id" => TermDefinition.new("id", id: "@id", simple: true),
|
19
|
+
"ignoredProperties" => TermDefinition.new("ignoredProperties", id: "http://www.w3.org/ns/shacl#ignoredProperties", type_mapping: "@id", container_mapping: "@list"),
|
20
|
+
"in" => TermDefinition.new("in", id: "http://www.w3.org/ns/shacl#in", type_mapping: "@none", container_mapping: "@list"),
|
21
|
+
"inversePath" => TermDefinition.new("inversePath", id: "http://www.w3.org/ns/shacl#inversePath", type_mapping: "@id"),
|
22
|
+
"label" => TermDefinition.new("label", id: "http://www.w3.org/2000/01/rdf-schema#label", simple: true),
|
23
|
+
"languageIn" => TermDefinition.new("languageIn", id: "http://www.w3.org/ns/shacl#languageIn", container_mapping: "@list"),
|
24
|
+
"lessThan" => TermDefinition.new("lessThan", id: "http://www.w3.org/ns/shacl#lessThan", type_mapping: "@id"),
|
25
|
+
"lessThanOrEquals" => TermDefinition.new("lessThanOrEquals", id: "http://www.w3.org/ns/shacl#lessThanOrEquals", type_mapping: "@id"),
|
26
|
+
"nodeKind" => TermDefinition.new("nodeKind", id: "http://www.w3.org/ns/shacl#nodeKind", type_mapping: "@vocab"),
|
27
|
+
"or" => TermDefinition.new("or", id: "http://www.w3.org/ns/shacl#or", type_mapping: "@id", container_mapping: "@list"),
|
28
|
+
"path" => TermDefinition.new("path", id: "http://www.w3.org/ns/shacl#path", type_mapping: "@none"),
|
29
|
+
"property" => TermDefinition.new("property", id: "http://www.w3.org/ns/shacl#property", type_mapping: "@id"),
|
30
|
+
"rdfs" => TermDefinition.new("rdfs", id: "http://www.w3.org/2000/01/rdf-schema#", simple: true, prefix: true),
|
31
|
+
"severity" => TermDefinition.new("severity", id: "http://www.w3.org/ns/shacl#severity", type_mapping: "@vocab"),
|
32
|
+
"sh" => TermDefinition.new("sh", id: "http://www.w3.org/ns/shacl#", simple: true, prefix: true),
|
33
|
+
"shacl" => TermDefinition.new("shacl", id: "http://www.w3.org/ns/shacl#", simple: true, prefix: true),
|
34
|
+
"targetClass" => TermDefinition.new("targetClass", id: "http://www.w3.org/ns/shacl#targetClass", type_mapping: "@id"),
|
35
|
+
"targetNode" => TermDefinition.new("targetNode", id: "http://www.w3.org/ns/shacl#targetNode", type_mapping: "@none"),
|
36
|
+
"type" => TermDefinition.new("type", id: "@type", container_mapping: "@set"),
|
37
|
+
"xone" => TermDefinition.new("xone", id: "http://www.w3.org/ns/shacl#xone", type_mapping: "@id", container_mapping: "@list"),
|
38
|
+
"xsd" => TermDefinition.new("xsd", id: "http://www.w3.org/2001/XMLSchema#", simple: true, prefix: true)
|
39
|
+
})
|
40
|
+
end
|
41
|
+
end
|
data/lib/shacl/format.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'rdf/format'
|
2
|
+
|
3
|
+
module SHACL
|
4
|
+
##
|
5
|
+
# SHACL format specification. Note that this format does not define any readers or writers.
|
6
|
+
#
|
7
|
+
# @example Obtaining an ShEx format class
|
8
|
+
# RDF::Format.for(:shacl) #=> ShEx::Format
|
9
|
+
class Format < RDF::Format
|
10
|
+
##
|
11
|
+
# Hash of CLI commands appropriate for this format
|
12
|
+
# @return [Hash{Symbol => Lambda(Array, Hash)}]
|
13
|
+
def self.cli_commands
|
14
|
+
{
|
15
|
+
shacl: {
|
16
|
+
description: "Validate repository given shape",
|
17
|
+
help: %(shacl [--shape URI] [--focus Resource] [--replace]
|
18
|
+
|
19
|
+
Evaluates the repository according the the specified shapes.
|
20
|
+
If no shape file is specified, it will look for one or more
|
21
|
+
shapes graphs using the sh:shapesGraph property found within
|
22
|
+
the repository.
|
23
|
+
).gsub(/^\s+/, ''),
|
24
|
+
parse: true,
|
25
|
+
lambda: -> (argv, **options) do
|
26
|
+
shacl = case options[:shape]
|
27
|
+
when IO, StringIO
|
28
|
+
SHACL.get_shapes(RDF::Reader.new(options[:shape]), **options)
|
29
|
+
when nil
|
30
|
+
SHACL.from_queryable(RDF::CLI.repository, **options)
|
31
|
+
else SHACL.open(options[:shape], **options)
|
32
|
+
end
|
33
|
+
|
34
|
+
if options[:to_sxp]
|
35
|
+
options[:messages][:shacl] = {}
|
36
|
+
options[:messages][:shacl].merge!({"S-Expression": [SXP::Generator.string(shacl.to_sxp_bin)]})
|
37
|
+
else
|
38
|
+
start = Time.now
|
39
|
+
report = shacl.execute(RDF::CLI.repository, **options)
|
40
|
+
secs = Time.new - start
|
41
|
+
options[:logger].info "SHACL resulted in #{report.conform? ? 'success' : 'failure'} including #{report.count} results."
|
42
|
+
options[:logger].info "Validated in #{secs} seconds."
|
43
|
+
options[:messages][:shacl] = {result: report.conform? ? "Satisfied shape" : "Did not satisfy shape"}
|
44
|
+
if report.conform?
|
45
|
+
options[:messages][:shacl] = {result: ["Satisfied shape"]}
|
46
|
+
else
|
47
|
+
RDF::CLI.repository << report
|
48
|
+
options[:messages][:shacl] = {result: ["Did not satisfy shape: #{report.count} results"]}
|
49
|
+
options[:messages].merge!(report.linter_messages)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
RDF::CLI.repository
|
53
|
+
end,
|
54
|
+
options: [
|
55
|
+
RDF::CLI::Option.new(
|
56
|
+
symbol: :focus,
|
57
|
+
datatype: String,
|
58
|
+
control: :text,
|
59
|
+
on: ["--focus Resource"],
|
60
|
+
description: "Focus node within repository"
|
61
|
+
) {|v| RDF::URI(v)},
|
62
|
+
RDF::CLI::Option.new(
|
63
|
+
symbol: :replace,
|
64
|
+
datatype: TrueClass,
|
65
|
+
control: :checkbox,
|
66
|
+
on: ["--replace"],
|
67
|
+
description: "Replaces the data graph with the validation report"
|
68
|
+
) {|v| RDF::URI(v)},
|
69
|
+
RDF::CLI::Option.new(
|
70
|
+
symbol: :shape,
|
71
|
+
datatype: String,
|
72
|
+
control: :url2,
|
73
|
+
on: ["--shape URI"],
|
74
|
+
description: "SHACL shapes graph location"
|
75
|
+
) {|v| RDF::URI(v)},
|
76
|
+
RDF::CLI::Option.new(
|
77
|
+
symbol: :to_sxp,
|
78
|
+
datatype: String,
|
79
|
+
control: :checkbox,
|
80
|
+
on: ["--to-sxp"],
|
81
|
+
description: "Display parsed shapes as an S-Expression"
|
82
|
+
),
|
83
|
+
]
|
84
|
+
}
|
85
|
+
}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
# Localized refinements to externally defined classes
|
2
|
+
module SHACL::Refinements
|
3
|
+
using SHACL::Refinements
|
4
|
+
|
5
|
+
refine Hash do
|
6
|
+
# @!parse
|
7
|
+
# # Refinements on Hash
|
8
|
+
# class Hash
|
9
|
+
# ##
|
10
|
+
# # Deep merge two hashes folding array values together.
|
11
|
+
# #
|
12
|
+
# # @param [Hash] second
|
13
|
+
# # @return [Hash]
|
14
|
+
# def deep_merge(second); end
|
15
|
+
# end
|
16
|
+
def deep_merge(second)
|
17
|
+
merger = ->(_, v1, v2) {Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : Array === v1 && Array === v2 ? v1 | v2 : v2.nil? ? v1 : v2 }
|
18
|
+
merge(second.to_h, &merger)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
refine SPARQL::Algebra::Operator::Alt do
|
23
|
+
# @!parse
|
24
|
+
# # Refinements on SPARQL::Algebra::Operator::Alt
|
25
|
+
# class SPARQL::Algebra::Operator::Alt
|
26
|
+
# ##
|
27
|
+
# # Retrieve the possibly newly assigned blank node subject to use for representing this operator.
|
28
|
+
# # @return [RDF::Node]
|
29
|
+
# attr_accessor :subject
|
30
|
+
#
|
31
|
+
# ##
|
32
|
+
# # Generate the SHACL representation of this operator
|
33
|
+
# # @return [RDF::Node]
|
34
|
+
# def each_statement(&block); end.
|
35
|
+
# end
|
36
|
+
attr_accessor :subject
|
37
|
+
def each_statement(&block)
|
38
|
+
@subject = RDF::Node.new
|
39
|
+
elements = operands.map do |op|
|
40
|
+
if op.respond_to?(:each_statement)
|
41
|
+
op.each_statement(&block)
|
42
|
+
op.subject
|
43
|
+
else
|
44
|
+
op
|
45
|
+
end
|
46
|
+
end
|
47
|
+
list = RDF::List(*elements)
|
48
|
+
list.each_statement(&block)
|
49
|
+
block.call(RDF::Statement(@subject, RDF::Vocab::SHACL.alternativePath, list.subject))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
refine SPARQL::Algebra::Operator::PathOpt do
|
54
|
+
# @!parse
|
55
|
+
# # Refinements on SPARQL::Algebra::Operator::PathOpt
|
56
|
+
# class SPARQL::Algebra::Operator::Alt
|
57
|
+
# ##
|
58
|
+
# # Retrieve the possibly newly assigned blank node subject to use for representing this operator.
|
59
|
+
# # @return [RDF::Node]
|
60
|
+
# attr_accessor :subject
|
61
|
+
#
|
62
|
+
# ##
|
63
|
+
# # Generate the SHACL representation of this operator
|
64
|
+
# # @return [RDF::Node]
|
65
|
+
# def each_statement(&block); end.
|
66
|
+
# end
|
67
|
+
attr_accessor :subject
|
68
|
+
def each_statement(&block)
|
69
|
+
@subject = RDF::Node.new
|
70
|
+
operands.each do |op|
|
71
|
+
obj = if op.respond_to?(:each_statement)
|
72
|
+
op.each_statement(&block)
|
73
|
+
op.subject
|
74
|
+
else
|
75
|
+
op
|
76
|
+
end
|
77
|
+
block.call(RDF::Statement(@subject, RDF::Vocab::SHACL.zeroOrOnePath, obj))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
refine SPARQL::Algebra::Operator::PathPlus do
|
83
|
+
# @!parse
|
84
|
+
# # Refinements on SPARQL::Algebra::Operator::PathPlus
|
85
|
+
# class SPARQL::Algebra::Operator::Alt
|
86
|
+
# ##
|
87
|
+
# # Retrieve the possibly newly assigned blank node subject to use for representing this operator.
|
88
|
+
# # @return [RDF::Node]
|
89
|
+
# attr_accessor :subject
|
90
|
+
#
|
91
|
+
# ##
|
92
|
+
# # Generate the SHACL representation of this operator
|
93
|
+
# # @return [RDF::Node]
|
94
|
+
# def each_statement(&block); end.
|
95
|
+
# end
|
96
|
+
attr_accessor :subject
|
97
|
+
def each_statement(&block)
|
98
|
+
@subject = RDF::Node.new
|
99
|
+
operands.each do |op|
|
100
|
+
obj = if op.respond_to?(:each_statement)
|
101
|
+
op.each_statement(&block)
|
102
|
+
op.subject
|
103
|
+
else
|
104
|
+
op
|
105
|
+
end
|
106
|
+
block.call(RDF::Statement(@subject, RDF::Vocab::SHACL.oneOrMorePath, obj))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
refine SPARQL::Algebra::Operator::PathStar do
|
112
|
+
# @!parse
|
113
|
+
# # Refinements on SPARQL::Algebra::Operator::PathPlus
|
114
|
+
# class SPARQL::Algebra::Operator::Alt
|
115
|
+
# ##
|
116
|
+
# # Retrieve the possibly newly assigned blank node subject to use for representing this operator.
|
117
|
+
# # @return [RDF::Node]
|
118
|
+
# attr_accessor :subject
|
119
|
+
#
|
120
|
+
# ##
|
121
|
+
# # Generate the SHACL representation of this operator
|
122
|
+
# # @return [RDF::Node]
|
123
|
+
# def each_statement(&block); end.
|
124
|
+
# end
|
125
|
+
attr_accessor :subject
|
126
|
+
def each_statement(&block)
|
127
|
+
@subject = RDF::Node.new
|
128
|
+
operands.each do |op|
|
129
|
+
obj = if op.respond_to?(:each_statement)
|
130
|
+
op.each_statement(&block)
|
131
|
+
op.subject
|
132
|
+
else
|
133
|
+
op
|
134
|
+
end
|
135
|
+
block.call(RDF::Statement(@subject, RDF::Vocab::SHACL.zeroOrMorePath, obj))
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
refine SPARQL::Algebra::Operator::Reverse do
|
141
|
+
# @!parse
|
142
|
+
# # Refinements on SPARQL::Algebra::Operator::Reverse
|
143
|
+
# class SPARQL::Algebra::Operator::Alt
|
144
|
+
# ##
|
145
|
+
# # Retrieve the possibly newly assigned blank node subject to use for representing this operator.
|
146
|
+
# # @return [RDF::Node]
|
147
|
+
# attr_accessor :subject
|
148
|
+
#
|
149
|
+
# ##
|
150
|
+
# # Generate the SHACL representation of this operator
|
151
|
+
# # @return [RDF::Node]
|
152
|
+
# def each_statement(&block); end.
|
153
|
+
# end
|
154
|
+
attr_accessor :subject
|
155
|
+
def each_statement(&block)
|
156
|
+
@subject = RDF::Node.new
|
157
|
+
operands.each do |op|
|
158
|
+
obj = if op.respond_to?(:each_statement)
|
159
|
+
op.each_statement(&block)
|
160
|
+
op.subject
|
161
|
+
else
|
162
|
+
op
|
163
|
+
end
|
164
|
+
block.call(RDF::Statement(@subject, RDF::Vocab::SHACL.inversePath, obj))
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
refine SPARQL::Algebra::Operator::Seq do
|
170
|
+
# @!parse
|
171
|
+
# # Refinements on SPARQL::Algebra::Operator::Seq
|
172
|
+
# class SPARQL::Algebra::Operator::Alt
|
173
|
+
# ##
|
174
|
+
# # Retrieve the possibly newly assigned blank node subject to use for representing this operator.
|
175
|
+
# # @return [RDF::Node]
|
176
|
+
# attr_accessor :subject
|
177
|
+
#
|
178
|
+
# ##
|
179
|
+
# # Generate the SHACL representation of this operator
|
180
|
+
# # @return [RDF::Node]
|
181
|
+
# def each_statement(&block); end.
|
182
|
+
# end
|
183
|
+
attr_accessor :subject
|
184
|
+
def each_statement(&block)
|
185
|
+
elements = operands.map do |op|
|
186
|
+
if op.respond_to?(:each_statement)
|
187
|
+
op.each_statement(&block)
|
188
|
+
op.subject
|
189
|
+
else
|
190
|
+
op
|
191
|
+
end
|
192
|
+
end
|
193
|
+
list = RDF::List(*elements)
|
194
|
+
list.each_statement(&block)
|
195
|
+
@subject = list.subject
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
data/lib/shacl/shapes.rb
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
require_relative 'algebra'
|
2
|
+
require_relative 'validation_report'
|
3
|
+
require_relative 'context'
|
4
|
+
require 'json/ld'
|
5
|
+
|
6
|
+
module SHACL
|
7
|
+
##
|
8
|
+
# The set of shapes loaded from a graph.
|
9
|
+
class Shapes < Array
|
10
|
+
include RDF::Util::Logger
|
11
|
+
|
12
|
+
# The graphs which have been loaded as shapes
|
13
|
+
#
|
14
|
+
# @return [Array<RDF::URI>]
|
15
|
+
attr_reader :loaded_graphs
|
16
|
+
|
17
|
+
# The JSON used to instantiate shapes
|
18
|
+
#
|
19
|
+
# @return [Array<Hash>]
|
20
|
+
attr_reader :shape_json
|
21
|
+
|
22
|
+
##
|
23
|
+
# Initializes the shapes from `graph`loading `owl:imports` until all references are loaded.
|
24
|
+
#
|
25
|
+
# The shapes come from the following:
|
26
|
+
# * Instances of `sh:NodeShape` or `sh:PropertyShape`
|
27
|
+
# * resources that have any of the properties `sh:targetClass`, `sh:targetNode`, `sh:targetObjectsOf`, or `sh:targetSubjectsOf`.
|
28
|
+
#
|
29
|
+
# @param [RDF::Graph] graph
|
30
|
+
# @param [Array<RDF::URI>] loaded_graphs = []
|
31
|
+
# @param [Hash{Symbol => Object}] options
|
32
|
+
# @return [Shapes]
|
33
|
+
# @raise [SHACL::Error]
|
34
|
+
def self.from_graph(graph, loaded_graphs: [], **options)
|
35
|
+
@loded_graphs = loaded_graphs
|
36
|
+
|
37
|
+
import_count = 0
|
38
|
+
while (imports = graph.query(predicate: RDF::OWL.imports).map(&:object)).count > import_count
|
39
|
+
# Load each imported graph
|
40
|
+
imports.each do |ref|
|
41
|
+
graph.load(imports)
|
42
|
+
loaded_graphs << ref
|
43
|
+
import_count += 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Serialize the graph as framed JSON-LD and initialize patterns, recursively.
|
48
|
+
shape_json = JSON::LD::API.fromRdf(graph, useNativeTypes: true) do |expanded|
|
49
|
+
JSON::LD::API.frame(expanded, SHAPES_FRAME, omitGraph: false, embed: '@always', expanded: true)
|
50
|
+
end['@graph']
|
51
|
+
|
52
|
+
# Create an array of the framed shapes
|
53
|
+
shapes = self.new(shape_json.map {|o| Algebra.from_json(o, **options)})
|
54
|
+
shapes.instance_variable_set(:@shape_json, shape_json)
|
55
|
+
shapes
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Retrieve shapes from a sh:shapesGraph reference within the queryable
|
60
|
+
#
|
61
|
+
# @param [RDF::Queryable] queryable
|
62
|
+
# The data graph which may contain references to the shapes graph
|
63
|
+
# @param [Hash{Symbol => Object}] options
|
64
|
+
# @return [Shapes]
|
65
|
+
# @raise [SHACL::Error]
|
66
|
+
def self.from_queryable(queryable, **options)
|
67
|
+
# Query queryable to find one ore more shapes graphs
|
68
|
+
graphs = queryable.query(predicate: RDF::Vocab::SHACL.shapesGraph).objects
|
69
|
+
graph = RDF::Graph.new do |g|
|
70
|
+
graphs.each {|iri| g.load(iri)}
|
71
|
+
end
|
72
|
+
from_graph(graph, loaded_graphs: graphs, **options)
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Match on schema. Finds appropriate shape for node, and matches that shape.
|
77
|
+
#
|
78
|
+
# @param [RDF::Queryable] graph
|
79
|
+
# @return [Hash{RDF::Term => Array<ValidationResult>}] Returns _ValidationResults_, a hash of focus nodes to the results of their associated shapes
|
80
|
+
# @param [Hash{Symbol => Object}] options
|
81
|
+
# @option options [RDF::Term] :focus
|
82
|
+
# An explicit focus node, overriding any defined on the top-level shaps.
|
83
|
+
# @return [SHACL::ValidationReport]
|
84
|
+
def execute(graph, depth: 0, **options)
|
85
|
+
self.each do |shape|
|
86
|
+
shape.graph = graph
|
87
|
+
shape.each_descendant do |op|
|
88
|
+
op.graph = graph
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Execute all shapes against their target nodes
|
93
|
+
ValidationReport.new(self.map do |shape|
|
94
|
+
nodes = Array(options.fetch(:focus, shape.targetNodes))
|
95
|
+
nodes.map do |node|
|
96
|
+
shape.conforms(node, depth: depth + 1)
|
97
|
+
end
|
98
|
+
end.flatten)
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_sxp_bin
|
102
|
+
[:shapes, super]
|
103
|
+
end
|
104
|
+
|
105
|
+
def to_sxp
|
106
|
+
to_sxp_bin.to_sxp
|
107
|
+
end
|
108
|
+
|
109
|
+
SHAPES_FRAME = JSON.parse(%({
|
110
|
+
"@context": {
|
111
|
+
"id": "@id",
|
112
|
+
"type": {"@id": "@type", "@container": "@set"},
|
113
|
+
"@vocab": "http://www.w3.org/ns/shacl#",
|
114
|
+
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
|
115
|
+
"shacl": "http://www.w3.org/ns/shacl#",
|
116
|
+
"sh": "http://www.w3.org/ns/shacl#",
|
117
|
+
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
118
|
+
"and": {"@type": "@id", "@container": "@list"},
|
119
|
+
"annotationProperty": {"@type": "@id"},
|
120
|
+
"class": {"@type": "@id"},
|
121
|
+
"comment": "http://www.w3.org/2000/01/rdf-schema#comment",
|
122
|
+
"condition": {"@type": "@id"},
|
123
|
+
"datatype": {"@type": "@vocab"},
|
124
|
+
"declare": {"@type": "@id"},
|
125
|
+
"disjoint": {"@type": "@id"},
|
126
|
+
"entailment": {"@type": "@id"},
|
127
|
+
"equals": {"@type": "@id"},
|
128
|
+
"ignoredProperties": {"@type": "@id", "@container": "@list"},
|
129
|
+
"in": {"@type": "@none", "@container": "@list"},
|
130
|
+
"inversePath": {"@type": "@id"},
|
131
|
+
"label": "http://www.w3.org/2000/01/rdf-schema#label",
|
132
|
+
"languageIn": {"@container": "@list"},
|
133
|
+
"lessThan": {"@type": "@id"},
|
134
|
+
"lessThanOrEquals": {"@type": "@id"},
|
135
|
+
"nodeKind": {"@type": "@vocab"},
|
136
|
+
"or": {"@type": "@id", "@container": "@list"},
|
137
|
+
"path": {"@type": "@none"},
|
138
|
+
"property": {"@type": "@id"},
|
139
|
+
"severity": {"@type": "@vocab"},
|
140
|
+
"targetClass": {"@type": "@id"},
|
141
|
+
"targetNode": {"@type": "@none"},
|
142
|
+
"xone": {"@type": "@id", "@container": "@list"}
|
143
|
+
},
|
144
|
+
"and": {},
|
145
|
+
"class": {},
|
146
|
+
"datatype": {},
|
147
|
+
"in": {},
|
148
|
+
"node": {},
|
149
|
+
"nodeKind": {},
|
150
|
+
"not": {},
|
151
|
+
"or": {},
|
152
|
+
"property": {},
|
153
|
+
"targetClass": {},
|
154
|
+
"targetNode": {},
|
155
|
+
"targetObjectsOf": {},
|
156
|
+
"xone": {},
|
157
|
+
"targetSubjectsOf": {}
|
158
|
+
})).freeze
|
159
|
+
end
|
160
|
+
end
|