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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f1980413b4fbf92e655601081fbc493d9a5dcb77
4
- data.tar.gz: 545ce25a9e024691ee7b8d7e0166921bfa0f876a
3
+ metadata.gz: 82811d1af73f62458a8c0ae0b719ff6a0ea14a3f
4
+ data.tar.gz: 37ba1a731d2a5435e9fb61d0a2caa7b008b58d51
5
5
  SHA512:
6
- metadata.gz: e4e95ad17224135fee1f801455eaa802a277efb82227f9d4837f3eccdeeb6b5a3c7c5a4f9686ca6dbed50afd3ea67dd625d30150b2299ff15ac56d45fef4f883
7
- data.tar.gz: 19fb975b67c1e0aaff517e98f9513c42b6ca618272d8d862fbcc44b104ad62f33fc2442ae9d835ac70dc724bc24ba6d9ccd55ddc061e1d0e56798a781477ddd5
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.2.2
1
+ 0.3.0
data/lib/rdf/reasoner.rb CHANGED
@@ -41,6 +41,13 @@ module RDF
41
41
  end
42
42
  module_function :apply
43
43
 
44
+ ##
45
+ # Add all entailment regimes
46
+ def apply_all
47
+ apply(*%w(rdfs owl schema))
48
+ end
49
+ module_function :apply_all
50
+
44
51
  ##
45
52
  # A reasoner error
46
53
  class Error < RuntimeError; end
@@ -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 Registered entailment method(s)
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 = @@entailments.keys if rules.empty?
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>] *rules Registered entailment method(s)
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>] *rules Registered entailment method(s)
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 = @@entailments.keys if rules.empty?
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 *statements
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
@@ -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 []
@@ -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.2.2
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-05-27 00:00:00.000000000 Z
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.7
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