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
@@ -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
|