rdf-reasoner 0.2.2 → 0.3.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 +4 -4
- data/README.md +17 -1
- data/VERSION +1 -1
- data/lib/rdf/reasoner.rb +7 -0
- data/lib/rdf/reasoner/extensions.rb +99 -6
- data/lib/rdf/reasoner/rdfs.rb +4 -0
- data/lib/rdf/reasoner/schema.rb +26 -26
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 82811d1af73f62458a8c0ae0b719ff6a0ea14a3f
|
4
|
+
data.tar.gz: 37ba1a731d2a5435e9fb61d0a2caa7b008b58d51
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 93b537c3d167f0cdd99d19163b40caa33a2a611e9b6a5f3550ef29d211472215cfaa5fabce00dbde74a3865ffd8a82f8c00394899e9a551ef9c5c3fadda46aba
|
7
|
+
data.tar.gz: facc4fbf521fb828f83488a13dd713b051576350fc959c4fd4493aeb954c4f96c294a0a1f6c4ce17a6489adf75b223d4ec6cd2947dbf7203ccb2bb4d66f53e93
|
data/README.md
CHANGED
@@ -40,7 +40,7 @@ Domain and Range entailment include specific rules for schema.org vocabularies.
|
|
40
40
|
require 'rdf/reasoner'
|
41
41
|
|
42
42
|
RDF::Reasoner.apply(:rdfs)
|
43
|
-
term = RDF::FOAF.Person
|
43
|
+
term = RDF::Vocab::FOAF.Person
|
44
44
|
term.entail(:subClass) # => [foaf:Person, mo:SoloMusicArtist]
|
45
45
|
|
46
46
|
### Determine if a resource is compatible with the domains of a property
|
@@ -81,6 +81,22 @@ Domain and Range entailment include specific rules for schema.org vocabularies.
|
|
81
81
|
graph = RDF::Graph.load("etc/doap.ttl")
|
82
82
|
graph.enum_statement.entail.count # >= graph.enum_statement.count
|
83
83
|
|
84
|
+
### Lint an expanded graph
|
85
|
+
|
86
|
+
require 'rdf/reasoner'
|
87
|
+
require 'rdf/turtle'
|
88
|
+
|
89
|
+
RDF::Reasoner.apply(:rdfs, :owl)
|
90
|
+
graph = RDF::Graph.load("etc/doap.ttl")
|
91
|
+
graph.entail!
|
92
|
+
messages = graph.lint
|
93
|
+
messages.each do |kind, term_messages|
|
94
|
+
term_messages.each do |term, messages|
|
95
|
+
options[:output].puts "#{kind} #{term}"
|
96
|
+
messages.each {|m| options[:output].puts " #{m}"}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
84
100
|
## Dependencies
|
85
101
|
|
86
102
|
* [Ruby](http://ruby-lang.org/) (>= 1.9.2)
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/lib/rdf/reasoner.rb
CHANGED
@@ -101,7 +101,9 @@ module RDF
|
|
101
101
|
# For best results, either run rules separately expanding the enumberated graph, or run repeatedly until no new statements are added to the enumerable containing both original and entailed statements. As `:subClassOf` and `:subPropertyOf` entailments are implicitly recursive, this may not be necessary except for extreme cases.
|
102
102
|
#
|
103
103
|
# @overload entail
|
104
|
-
# @param [Array<Symbol>] *rules
|
104
|
+
# @param [Array<Symbol>] *rules
|
105
|
+
# Registered entailment method(s).
|
106
|
+
#
|
105
107
|
# @yield statement
|
106
108
|
# @yieldparam [RDF::Statement] statement
|
107
109
|
# @return [void]
|
@@ -111,7 +113,7 @@ module RDF
|
|
111
113
|
# @return [Enumerator]
|
112
114
|
def entail(*rules, &block)
|
113
115
|
if block_given?
|
114
|
-
rules =
|
116
|
+
rules = %w(subClassOf subPropertyOf domain range).map(&:to_sym) if rules.empty?
|
115
117
|
|
116
118
|
self.each do |statement|
|
117
119
|
rules.each {|rule| statement.entail(rule, &block)}
|
@@ -141,7 +143,7 @@ module RDF
|
|
141
143
|
|
142
144
|
# Return a new mutable, composed of original and entailed statements
|
143
145
|
#
|
144
|
-
# @param [Array<Symbol>]
|
146
|
+
# @param [Array<Symbol>] rules Registered entailment method(s)
|
145
147
|
# @return [RDF::Mutable]
|
146
148
|
# @see [RDF::Enumerable#entail]
|
147
149
|
def entail(*rules, &block)
|
@@ -150,11 +152,11 @@ module RDF
|
|
150
152
|
|
151
153
|
# Add entailed statements to the mutable
|
152
154
|
#
|
153
|
-
# @param [Array<Symbol>]
|
155
|
+
# @param [Array<Symbol>] rules Registered entailment method(s)
|
154
156
|
# @return [RDF::Mutable]
|
155
157
|
# @see [RDF::Enumerable#entail]
|
156
158
|
def entail!(*rules, &block)
|
157
|
-
rules =
|
159
|
+
rules = %w(subClassOf subPropertyOf domain range).map(&:to_sym) if rules.empty?
|
158
160
|
statements = []
|
159
161
|
|
160
162
|
self.each do |statement|
|
@@ -164,8 +166,99 @@ module RDF
|
|
164
166
|
end
|
165
167
|
end
|
166
168
|
end
|
167
|
-
self.insert
|
169
|
+
self.insert(*statements)
|
168
170
|
self
|
169
171
|
end
|
170
172
|
end
|
173
|
+
|
174
|
+
module Queryable
|
175
|
+
# Lint a queryable, presuming that it has already had RDFS entailment expansion.
|
176
|
+
# @return [Hash{Symbol => Hash{Symbol => Array<String>}}] messages found for classes and properties by term
|
177
|
+
def lint
|
178
|
+
messages = {}
|
179
|
+
|
180
|
+
# Check for defined classes in known vocabularies
|
181
|
+
self.query(:predicate => RDF.type) do |stmt|
|
182
|
+
vocab = RDF::Vocabulary.find(stmt.object)
|
183
|
+
term = (RDF::Vocabulary.find_term(stmt.object) rescue nil) if vocab
|
184
|
+
pname = term ? term.pname : stmt.object.pname
|
185
|
+
|
186
|
+
# Must be a defined term, not in RDF or RDFS vocabularies
|
187
|
+
if term && term.class?
|
188
|
+
# Warn against using a deprecated term
|
189
|
+
superseded = term.attributes['schema:supersededBy']
|
190
|
+
(messages[:class] ||= {})[pname] = ["Term is superseded by #{superseded}"] if superseded
|
191
|
+
else
|
192
|
+
(messages[:class] ||= {})[pname] = ["No class definition found"] unless vocab.nil? || [RDF::RDFV, RDF::RDFS].include?(vocab)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Check for defined predicates in known vocabularies and domain/range
|
197
|
+
resource_types = {}
|
198
|
+
self.each_statement do |stmt|
|
199
|
+
vocab = RDF::Vocabulary.find(stmt.predicate)
|
200
|
+
term = (RDF::Vocabulary.find_term(stmt.predicate) rescue nil) if vocab
|
201
|
+
pname = term ? term.pname : stmt.predicate.pname
|
202
|
+
|
203
|
+
# Must be a defined property
|
204
|
+
if term && term.property?
|
205
|
+
# Warn against using a deprecated term
|
206
|
+
superseded = term.attributes['schema:supersededBy']
|
207
|
+
(messages[:property] ||= {})[pname] = ["Term is superseded by #{superseded}"] if superseded
|
208
|
+
else
|
209
|
+
((messages[:property] ||= {})[pname] ||= []) << "No property definition found" unless vocab.nil?
|
210
|
+
next
|
211
|
+
end
|
212
|
+
|
213
|
+
# See if type of the subject is in the domain of this predicate
|
214
|
+
resource_types[stmt.subject] ||= self.query(subject: stmt.subject, predicate: RDF.type).
|
215
|
+
map {|s| (t = (RDF::Vocabulary.find_term(s.object) rescue nil)) && t.entail(:subClassOf)}.
|
216
|
+
flatten.
|
217
|
+
uniq.
|
218
|
+
compact
|
219
|
+
|
220
|
+
unless term.domain_compatible?(stmt.subject, self, :types => resource_types[stmt.subject])
|
221
|
+
((messages[:property] ||= {})[pname] ||= []) << if term.respond_to?(:domain)
|
222
|
+
"Subject #{show_resource(stmt.subject)} not compatible with domain (#{Array(term.domain).map {|d| d.pname|| d}.join(',')})"
|
223
|
+
else
|
224
|
+
"Subject #{show_resource(stmt.subject)} not compatible with domainIncludes (#{term.domainIncludes.map {|d| d.pname|| d}.join(',')})"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Make sure that if ranges are defined, the object has an appropriate type
|
229
|
+
resource_types[stmt.object] ||= self.query(subject: stmt.object, predicate: RDF.type).
|
230
|
+
map {|s| (t = (RDF::Vocabulary.find_term(s.object) rescue nil)) && t.entail(:subClassOf)}.
|
231
|
+
flatten.
|
232
|
+
uniq.
|
233
|
+
compact if stmt.object.resource?
|
234
|
+
|
235
|
+
unless term.range_compatible?(stmt.object, self, :types => resource_types[stmt.object])
|
236
|
+
((messages[:property] ||= {})[pname] ||= []) << if term.respond_to?(:range)
|
237
|
+
"Object #{show_resource(stmt.object)} not compatible with range (#{Array(term.range).map {|d| d.pname|| d}.join(',')})"
|
238
|
+
else
|
239
|
+
"Object #{show_resource(stmt.object)} not compatible with rangeIncludes (#{term.rangeIncludes.map {|d| d.pname|| d}.join(',')})"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
messages[:class].each {|k, v| messages[:class][k] = v.uniq} if messages[:class]
|
245
|
+
messages[:property].each {|k, v| messages[:property][k] = v.uniq} if messages[:property]
|
246
|
+
messages
|
247
|
+
end
|
248
|
+
|
249
|
+
private
|
250
|
+
|
251
|
+
# Show resource in diagnostic output
|
252
|
+
def show_resource(resource)
|
253
|
+
if resource.node?
|
254
|
+
resource.to_ntriples + '(' +
|
255
|
+
self.query(subject: resource, predicate: RDF.type).
|
256
|
+
map {|s| s.object.uri? ? s.object.pname : s.object.to_ntriples}
|
257
|
+
.join(',') +
|
258
|
+
')'
|
259
|
+
else
|
260
|
+
resource.to_ntriples
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
171
264
|
end
|
data/lib/rdf/reasoner/rdfs.rb
CHANGED
@@ -61,6 +61,7 @@ module RDF::Reasoner
|
|
61
61
|
statements << RDF::Statement.new(self.to_hash.merge(object: t))
|
62
62
|
end
|
63
63
|
end
|
64
|
+
#$stderr.puts("subClassf(#{self.predicate.pname}): #{statements.map(&:object).map {|r| r.respond_to?(:pname) ? r.pname : r.to_ntriples}}}")
|
64
65
|
end
|
65
66
|
statements.each {|s| yield s} if block_given?
|
66
67
|
statements
|
@@ -128,6 +129,7 @@ module RDF::Reasoner
|
|
128
129
|
term._entail_subPropertyOf do |t|
|
129
130
|
statements << RDF::Statement.new(self.to_hash.merge(predicate: t))
|
130
131
|
end
|
132
|
+
#$stderr.puts("subPropertyOf(#{self.predicate.pname}): #{statements.map(&:object).map {|r| r.respond_to?(:pname) ? r.pname : r.to_ntriples}}}")
|
131
133
|
end
|
132
134
|
statements.each {|s| yield s} if block_given?
|
133
135
|
statements
|
@@ -147,6 +149,7 @@ module RDF::Reasoner
|
|
147
149
|
statements << RDF::Statement.new(self.to_hash.merge(predicate: RDF.type, object: t))
|
148
150
|
end
|
149
151
|
end
|
152
|
+
#$stderr.puts("domain(#{self.predicate.pname}): #{statements.map(&:object).map {|r| r.respond_to?(:pname) ? r.pname : r.to_ntriples}}}")
|
150
153
|
statements.each {|s| yield s} if block_given?
|
151
154
|
statements
|
152
155
|
else []
|
@@ -165,6 +168,7 @@ module RDF::Reasoner
|
|
165
168
|
statements << RDF::Statement.new(self.to_hash.merge(subject: self.object, predicate: RDF.type, object: t))
|
166
169
|
end
|
167
170
|
end
|
171
|
+
#$stderr.puts("range(#{self.predicate.pname}): #{statements.map(&:object).map {|r| r.respond_to?(:pname) ? r.pname : r.to_ntriples}}")
|
168
172
|
statements.each {|s| yield s} if block_given?
|
169
173
|
statements
|
170
174
|
else []
|
data/lib/rdf/reasoner/schema.rb
CHANGED
@@ -40,7 +40,7 @@ module RDF::Reasoner
|
|
40
40
|
|
41
41
|
# Resource may still be acceptable if types include schema:Role, and any any other resource references `resource` using this property
|
42
42
|
resource_acceptable ||
|
43
|
-
types.include?(RDF::SCHEMA.Role) &&
|
43
|
+
types.include?(RDF::Vocab::SCHEMA.Role) &&
|
44
44
|
!queryable.query(predicate: self, object: resource).empty?
|
45
45
|
end
|
46
46
|
|
@@ -65,11 +65,11 @@ module RDF::Reasoner
|
|
65
65
|
ranges.any? do |range|
|
66
66
|
case range
|
67
67
|
when RDF::RDFS.Literal then true
|
68
|
-
when RDF::SCHEMA.Text then resource.plain? || resource.datatype == RDF::SCHEMA.Text
|
69
|
-
when RDF::SCHEMA.Boolean
|
70
|
-
[RDF::SCHEMA.Boolean, RDF::XSD.boolean].include?(resource.datatype) ||
|
68
|
+
when RDF::Vocab::SCHEMA.Text then resource.plain? || resource.datatype == RDF::Vocab::SCHEMA.Text
|
69
|
+
when RDF::Vocab::SCHEMA.Boolean
|
70
|
+
[RDF::Vocab::SCHEMA.Boolean, RDF::XSD.boolean].include?(resource.datatype) ||
|
71
71
|
resource.plain? && RDF::Literal::Boolean.new(resource.value).valid?
|
72
|
-
when RDF::SCHEMA.Date
|
72
|
+
when RDF::Vocab::SCHEMA.Date
|
73
73
|
# Schema.org date based on ISO 8601, mapped to appropriate XSD types for validation
|
74
74
|
case resource
|
75
75
|
when RDF::Literal::Date, RDF::Literal::Time, RDF::Literal::DateTime, RDF::Literal::Duration
|
@@ -77,35 +77,35 @@ module RDF::Reasoner
|
|
77
77
|
else
|
78
78
|
ISO_8601.match(resource.value)
|
79
79
|
end
|
80
|
-
when RDF::SCHEMA.DateTime
|
81
|
-
resource.datatype == RDF::SCHEMA.DateTime ||
|
80
|
+
when RDF::Vocab::SCHEMA.DateTime
|
81
|
+
resource.datatype == RDF::Vocab::SCHEMA.DateTime ||
|
82
82
|
resource.is_a?(RDF::Literal::DateTime) ||
|
83
83
|
resource.plain? && RDF::Literal::DateTime.new(resource.value).valid?
|
84
|
-
when RDF::SCHEMA.Duration
|
84
|
+
when RDF::Vocab::SCHEMA.Duration
|
85
85
|
value = resource.value
|
86
86
|
value = "P#{value}" unless value.start_with?("P")
|
87
|
-
resource.datatype == RDF::SCHEMA.Duration ||
|
87
|
+
resource.datatype == RDF::Vocab::SCHEMA.Duration ||
|
88
88
|
resource.is_a?(RDF::Literal::Duration) ||
|
89
89
|
resource.plain? && RDF::Literal::Duration.new(value).valid?
|
90
|
-
when RDF::SCHEMA.Time
|
91
|
-
resource.datatype == RDF::SCHEMA.Time ||
|
90
|
+
when RDF::Vocab::SCHEMA.Time
|
91
|
+
resource.datatype == RDF::Vocab::SCHEMA.Time ||
|
92
92
|
resource.is_a?(RDF::Literal::Time) ||
|
93
93
|
resource.plain? && RDF::Literal::Time.new(resource.value).valid?
|
94
|
-
when RDF::SCHEMA.Number
|
94
|
+
when RDF::Vocab::SCHEMA.Number
|
95
95
|
resource.is_a?(RDF::Literal::Numeric) ||
|
96
|
-
[RDF::SCHEMA.Number, RDF::SCHEMA.Float, RDF::SCHEMA.Integer].include?(resource.datatype) ||
|
96
|
+
[RDF::Vocab::SCHEMA.Number, RDF::Vocab::SCHEMA.Float, RDF::Vocab::SCHEMA.Integer].include?(resource.datatype) ||
|
97
97
|
resource.plain? && RDF::Literal::Integer.new(resource.value).valid? ||
|
98
98
|
resource.plain? && RDF::Literal::Double.new(resource.value).valid?
|
99
|
-
when RDF::SCHEMA.Float
|
99
|
+
when RDF::Vocab::SCHEMA.Float
|
100
100
|
resource.is_a?(RDF::Literal::Double) ||
|
101
|
-
[RDF::SCHEMA.Number, RDF::SCHEMA.Float].include?(resource.datatype) ||
|
101
|
+
[RDF::Vocab::SCHEMA.Number, RDF::Vocab::SCHEMA.Float].include?(resource.datatype) ||
|
102
102
|
resource.plain? && RDF::Literal::Double.new(resource.value).valid?
|
103
|
-
when RDF::SCHEMA.Integer
|
103
|
+
when RDF::Vocab::SCHEMA.Integer
|
104
104
|
resource.is_a?(RDF::Literal::Integer) ||
|
105
|
-
[RDF::SCHEMA.Number, RDF::SCHEMA.Integer].include?(resource.datatype) ||
|
105
|
+
[RDF::Vocab::SCHEMA.Number, RDF::Vocab::SCHEMA.Integer].include?(resource.datatype) ||
|
106
106
|
resource.plain? && RDF::Literal::Integer.new(resource.value).valid?
|
107
|
-
when RDF::SCHEMA.URL
|
108
|
-
resource.datatype == RDF::SCHEMA.URL ||
|
107
|
+
when RDF::Vocab::SCHEMA.URL
|
108
|
+
resource.datatype == RDF::Vocab::SCHEMA.URL ||
|
109
109
|
resource.datatype == RDF::XSD.anyURI ||
|
110
110
|
resource.plain? && RDF::Literal::AnyURI.new(resource.value).valid?
|
111
111
|
else
|
@@ -123,11 +123,11 @@ module RDF::Reasoner
|
|
123
123
|
end
|
124
124
|
end
|
125
125
|
end
|
126
|
-
elsif %w(True False).map {|v| RDF::SCHEMA[v]}.include?(resource) && ranges.include?(RDF::SCHEMA.Boolean)
|
126
|
+
elsif %w(True False).map {|v| RDF::Vocab::SCHEMA[v]}.include?(resource) && ranges.include?(RDF::Vocab::SCHEMA.Boolean)
|
127
127
|
true # Special case for schema boolean resources
|
128
|
-
elsif ranges.include?(RDF::SCHEMA.URL) && resource.uri?
|
128
|
+
elsif ranges.include?(RDF::Vocab::SCHEMA.URL) && resource.uri?
|
129
129
|
true # schema:URL matches URI resources
|
130
|
-
elsif ranges.include?(RDF::SCHEMA.Text) && resource.uri?
|
130
|
+
elsif ranges.include?(RDF::Vocab::SCHEMA.Text) && resource.uri?
|
131
131
|
# Allowed if resource is untyped
|
132
132
|
# Fully entailed types of the resource
|
133
133
|
types = options.fetch(:types) do
|
@@ -157,7 +157,7 @@ module RDF::Reasoner
|
|
157
157
|
resource_acceptable ||
|
158
158
|
|
159
159
|
# Resource also acceptable if it is a Role, and the Role object contains the same predicate having a compatible object
|
160
|
-
types.include?(RDF::SCHEMA.Role) &&
|
160
|
+
types.include?(RDF::Vocab::SCHEMA.Role) &&
|
161
161
|
queryable.query(subject: resource, predicate: self).any? do |stmt|
|
162
162
|
acc = self.range_compatible_schema?(stmt.object, queryable)
|
163
163
|
acc
|
@@ -178,9 +178,9 @@ module RDF::Reasoner
|
|
178
178
|
def literal_range?(ranges)
|
179
179
|
ranges.all? do |range|
|
180
180
|
case range
|
181
|
-
when RDF::RDFS.Literal, RDF::SCHEMA.Text, RDF::SCHEMA.Boolean, RDF::SCHEMA.Date,
|
182
|
-
RDF::SCHEMA.DateTime, RDF::SCHEMA.Time, RDF::SCHEMA.URL,
|
183
|
-
RDF::SCHEMA.Number, RDF::SCHEMA.Float, RDF::SCHEMA.Integer
|
181
|
+
when RDF::RDFS.Literal, RDF::Vocab::SCHEMA.Text, RDF::Vocab::SCHEMA.Boolean, RDF::Vocab::SCHEMA.Date,
|
182
|
+
RDF::Vocab::SCHEMA.DateTime, RDF::Vocab::SCHEMA.Time, RDF::Vocab::SCHEMA.URL,
|
183
|
+
RDF::Vocab::SCHEMA.Number, RDF::Vocab::SCHEMA.Float, RDF::Vocab::SCHEMA.Integer
|
184
184
|
true
|
185
185
|
else
|
186
186
|
# If this is an XSD range, look for appropriate literal
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rdf-reasoner
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gregg Kellogg
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-10-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rdf
|
@@ -167,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
167
167
|
version: '0'
|
168
168
|
requirements: []
|
169
169
|
rubyforge_project:
|
170
|
-
rubygems_version: 2.4.
|
170
|
+
rubygems_version: 2.4.5.1
|
171
171
|
signing_key:
|
172
172
|
specification_version: 4
|
173
173
|
summary: RDFS/OWL Reasoner for RDF.rb
|