Module: RDF::Reasoner::RDFS

Defined in:
lib/rdf/reasoner/rdfs.rb

Overview

Rules for generating RDFS entailment triples

Extends ‘RDF::URI` and `RDF::Statement` with specific entailment capabilities

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(mod) ⇒ Object



360
361
362
363
364
365
366
367
# File 'lib/rdf/reasoner/rdfs.rb', line 360

def self.included(mod)
  mod.add_entailment :subClassOf, :_entail_subClassOf
  mod.add_entailment :subClass, :_entail_subClass
  mod.add_entailment :subPropertyOf, :_entail_subPropertyOf
  mod.add_entailment :subProperty, :_entail_subProperty
  mod.add_entailment :domain, :_entail_domain
  mod.add_entailment :range, :_entail_range
end

Instance Method Details

#_entail_domainObject

TODO:

Should be able to entail owl:unionOf, which is a BNode. This should be allowed, and also add BNode values of that node, recursively, similar to SPARQL concise_bounded_description.uu

For a Statement: yield or return inferred statements having an rdf:type of the domain of the statement predicate



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/rdf/reasoner/rdfs.rb', line 204

def _entail_domain
  case self
  when RDF::Statement
    statements = []
    if term = (RDF::Vocabulary.find_term(self.predicate) rescue nil)
      term.domain.each do |t|
        next if t.node? # Don't entail BNodes
        statements << RDF::Statement(**self.to_h.merge(predicate: RDF.type, object: t, inferred: true))
      end
    end
    #$stderr.puts("domain(#{self.predicate.pname}): #{statements.map(&:object).map {|r| r.respond_to?(:pname) ? r.pname : r.to_ntriples}}}")
    statements.each {|s| yield s} if block_given?
    statements
  else []
  end
end

#_entail_rangeObject

TODO:

Should be able to entail owl:unionOf, which is a BNode. This should be allowed, and also add BNode values of that node, recursively, similar to SPARQL concise_bounded_description.uu

For a Statement: if object is a resource, yield or return inferred statements having an rdf:type of the range of the statement predicate



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/rdf/reasoner/rdfs.rb', line 225

def _entail_range
  case self
  when RDF::Statement
    statements = []
    if object.resource? && term = (RDF::Vocabulary.find_term(self.predicate) rescue nil)
      term.range.each do |t|
        next if t.node? # Don't entail BNodes
        statements << RDF::Statement(**self.to_h.merge(subject: self.object, predicate: RDF.type, object: t, inferred: true))
      end
    end
    #$stderr.puts("range(#{self.predicate.pname}): #{statements.map(&:object).map {|r| r.respond_to?(:pname) ? r.pname : r.to_ntriples}}")
    statements.each {|s| yield s} if block_given?
    statements
  else []
  end
end

#_entail_subClassObject

For a Term: yield or return inferred subClass relationships by recursively applying to named sub classes to get a complete set of classes in the descendant chain of this class For a Statement: this is a no-op, as it’s not useful in this context



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/rdf/reasoner/rdfs.rb', line 92

def _entail_subClass
  case self
  when RDF::URI, RDF::Node
    unless class?
      yield self if block_given?
      return Array(self)
    end
    terms = descendant_cache[self] ||= (
      Array(self.subClass).
        map {|c| c._entail_subClass rescue c}.
        flatten +
      Array(self)
    ).compact
    terms.each {|t| yield t} if block_given?
    terms
  else []
  end
end

#_entail_subClassOfObject

TODO:

Should be able to entail owl:Restriction, which is a BNode. This should be allowed, and also add BNode values of that node, recursively, similar to SPARQL concise_bounded_description.uu

For a Term: yield or return inferred subClassOf relationships by recursively applying to named super classes to get a complete set of classes in the ancestor chain of this class For a Statement: if predicate is ‘rdf:types`, yield or return inferred statements having a subClassOf relationship to the type of this statement



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/rdf/reasoner/rdfs.rb', line 56

def _entail_subClassOf
  case self
  when RDF::URI, RDF::Node
    unless class?
      yield self if block_given?
      return Array(self)
    end
    terms = subClassOf_cache[self] ||= (
      Array(self.subClassOf).
        map {|c| c._entail_subClassOf rescue c}.
        flatten +
      Array(self)
    ).compact
    terms.each {|t| yield t} if block_given?
    terms
  when RDF::Statement
    statements = []
    if self.predicate == RDF.type
      if term = (RDF::Vocabulary.find_term(self.object) rescue nil)
        term._entail_subClassOf do |t|
          next if t.node? # Don't entail BNodes
          statements << RDF::Statement(**self.to_h.merge(object: t, inferred: true))
        end
      end
      #$stderr.puts("subClassf(#{self.predicate.pname}): #{statements.map(&:object).map {|r| r.respond_to?(:pname) ? r.pname : r.to_ntriples}}}")
    end
    statements.each {|s| yield s} if block_given?
    statements
  else []
  end
end

#_entail_subPropertyObject

For a Term: yield or return inferred subProperty relationships by recursively applying to named subproperties to get a complete set of properties in the descendant chain of this property

For a Statement: this is a no-op, as it’s not useful in this context



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/rdf/reasoner/rdfs.rb', line 164

def _entail_subProperty
  case self
  when RDF::URI, RDF::Node
    unless property?
      yield self if block_given?
      return Array(self)
    end

    terms = descendant_property_cache[self] ||= (
      Array(self.subProperty).map do |c|
        c._entail_subProperty rescue c
      end.flatten + Array(self)).compact

    terms.each {|t| yield t } if block_given?
    terms
  else []
  end
end

#_entail_subPropertyOfObject

For a Term: yield or return inferred subPropertyOf relationships by recursively applying to named super classes to get a complete set of classes in the ancestor chain of this class For a Statement: yield or return inferred statements having a subPropertyOf relationship to predicate of this statements



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/rdf/reasoner/rdfs.rb', line 127

def _entail_subPropertyOf
  case self
  when RDF::URI, RDF::Node
    unless property?
      yield self if block_given?
      return Array(self)
    end
    terms = subPropertyOf_cache[self] ||= (
      Array(self.subPropertyOf).
        map {|c| c._entail_subPropertyOf rescue c}.
        flatten +
      Array(self)
    ).compact
    terms.each {|t| yield t} if block_given?
    terms
  when RDF::Statement
    statements = []
    if term = (RDF::Vocabulary.find_term(self.predicate) rescue nil)
      term._entail_subPropertyOf do |t|
        statements << RDF::Statement(**self.to_h.merge(predicate: t, inferred: true))
      end
      #$stderr.puts("subPropertyOf(#{self.predicate.pname}): #{statements.map(&:object).map {|r| r.respond_to?(:pname) ? r.pname : r.to_ntriples}}}")
    end
    statements.each {|s| yield s} if block_given?
    statements
  else []
  end
end

#descendant_cacheRDF::Util::Cache

Returns:

  • (RDF::Util::Cache)


26
27
28
# File 'lib/rdf/reasoner/rdfs.rb', line 26

def descendant_cache
  @@descendant_cache ||= RDF::Util::Cache.new(-1)
end

#descendant_property_cacheRDF::Util::Cache

Returns:

  • (RDF::Util::Cache)


47
48
49
# File 'lib/rdf/reasoner/rdfs.rb', line 47

def descendant_property_cache
  @@descendant_property_cache ||= RDF::Util::Cache.new(-1)
end

#domain_compatible_rdfs?(resource, queryable, options = {}) ⇒ Boolean

RDFS requires that if the property has a domain, and the resource has a type that some type matches every domain.

Note that this is different than standard entailment, which simply asserts that the resource has every type in the domain, but this is more useful to check if published data is consistent with the vocabulary definition.

Parameters:

  • resource (RDF::Resource)
  • queryable (RDF::Queryable)
  • options (Hash{Symbol => Object}) (defaults to: {})

    ({})

Options Hash (options):

  • :types (Array<RDF::Vocabulary::Term>)

    Fully entailed types of resource, if not provided, they are queried

Returns:

  • (Boolean)

Raises:



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/rdf/reasoner/rdfs.rb', line 252

def domain_compatible_rdfs?(resource, queryable, options = {})
  raise RDF::Reasoner::Error, "#{self} can't get domains" unless property?
  domains = Array(self.domain).reject(&:node?) - [RDF::OWL.Thing, RDF::RDFS.Resource]

  # Fully entailed types of the resource
  types = options.fetch(:types) do
    queryable.query({subject: resource, predicate: RDF.type}).
      map {|s| (t = (RDF::Vocabulary.find_term(s.object)) rescue nil) && t.entail(:subClassOf)}.
      flatten.
      uniq.
      compact
  end unless domains.empty?

  # Every domain must match some entailed type
  Array(types).empty? || domains.all? {|d| types.include?(d)}
end

#range_compatible_rdfs?(resource, queryable, options = {}) ⇒ Boolean

RDFS requires that if the property has a range, and the resource has a type that some type matches every range. If the resource is a datatyped Literal, and the range includes a datatype, the resource must be consistent with that.

Note that this is different than standard entailment, which simply asserts that the resource has every type in the range, but this is more useful to check if published data is consistent with the vocabulary definition.

Parameters:

  • resource (RDF::Resource)
  • queryable (RDF::Queryable)
  • options (Hash{Symbol => Object}) (defaults to: {})

    ({})

Options Hash (options):

  • :types (Array<RDF::Vocabulary::Term>)

    Fully entailed types of resource, if not provided, they are queried

Returns:

  • (Boolean)

Raises:



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/rdf/reasoner/rdfs.rb', line 279

def range_compatible_rdfs?(resource, queryable, options = {})
  raise RDF::Reasoner::Error, "#{self} can't get ranges" unless property?
  if !(ranges = Array(self.range).reject(&:node?) - [RDF::OWL.Thing, RDF::RDFS.Resource]).empty?
    if resource.literal?
      ranges.all? do |range|
        if [RDF::RDFS.Literal, RDF.XMLLiteral, RDF.HTML].include?(range)
          true  # Don't bother checking for validity
        elsif range == RDF.langString
          # Value must have a language
          resource.has_language?
        elsif range.start_with?(RDF::XSD)
          # XSD types are valid if the datatype matches, or they are plain and valid according to the grammar of the range
            resource.datatype == range ||
            resource.plain? && RDF::Literal.new(resource.value, datatype: range).valid?
        elsif range.start_with?("http://ogp.me/ns/class#")
          case range
          when RDF::URI("http://ogp.me/ns/class#boolean_str")
            [RDF::URI("http://ogp.me/ns/class#boolean_str"), RDF::XSD.boolean].include?(resource.datatype) ||
            resource.plain? && RDF::Literal::Boolean.new(resource.value).valid?
          when RDF::URI("http://ogp.me/ns/class#date_time_str")
            # Schema.org date based on ISO 8601, mapped to appropriate XSD types for validation
            case resource
            when RDF::Literal::Date, RDF::Literal::Time, RDF::Literal::DateTime, RDF::Literal::Duration
              resource.valid?
            else
              ISO_8601.match(resource.value)
            end
          when RDF::URI("http://ogp.me/ns/class#determiner_str")
            # The lexical space: "", "the", "a", "an", and "auto".
            resource.plain? && (%w(the a an auto) + [""]).include?(resource.value)
          when RDF::URI("http://ogp.me/ns/class#float_str")
            # A string representation of a 64-bit signed floating point number.  Example lexical values include "1.234", "-1.234", "1.2e3", "-1.2e3", and "7E-10".
            [RDF::URI("http://ogp.me/ns/class#float_str"), RDF::Literal::Double, RDF::Literal::Float].include?(resource.datatype) ||
            resource.plain? && RDF::Literal::Double.new(resource.value).valid?
          when RDF::URI("http://ogp.me/ns/class#integer_str")
            resource.is_a?(RDF::Literal::Integer) ||
            [RDF::URI("http://ogp.me/ns/class#integer_str")].include?(resource.datatype) ||
            resource.plain? && RDF::Literal::Integer.new(resource.value).valid?
          when RDF::URI("http://ogp.me/ns/class#mime_type_str")
            # Valid mime type strings \(e.g., "application/mp3"\).
            [RDF::URI("http://ogp.me/ns/class#mime_type_str")].include?(resource.datatype) ||
            resource.plain? && resource.value =~ %r(^[\w\-\+]+/[\w\-\+]+$)
          when RDF::URI("http://ogp.me/ns/class#string")
            resource.plain?
          when RDF::URI("http://ogp.me/ns/class#url")
            # A string of Unicode characters forming a valid URL having the http or https scheme.
            u = RDF::URI(resource.value)
            resource.datatype == RDF::URI("http://ogp.me/ns/class#url") ||
            resource.datatype == RDF::XSD.anyURI ||
            resource.simple? && u.valid? && u.scheme.to_s =~ /^https?$/
          else
            # Unknown datatype
            false
          end
        else
          false
        end
      end
    else
      # Fully entailed types of the resource
      types = options.fetch(:types) do
        queryable.query({subject: resource, predicate: RDF.type}).
          map {|s| (t = (RDF::Vocabulary.find_term(s.object) rescue nil)) && t.entail(:subClassOf)}.
          flatten.
          uniq.
          compact
      end

      # If any type is a class, add rdfs:Class
      if types.any? {|t| t.is_a?(RDF::Vocabulary::Term) && t.class?} && !types.include?(RDF::RDFS.Class)
        types << RDF::RDFS.Class
      end

      # Every range must match some entailed type
      Array(types).empty? || ranges.all? {|d| types.include?(d)}
    end
  else
    true
  end
end

#subClassArray<RDF::Vocabulary::Term>

Get the immediate subclasses of this class.

This iterates over terms defined in the vocabulary of this term, as well as the vocabularies imported by this vocabulary.

Returns:

  • (Array<RDF::Vocabulary::Term>)

Raises:



116
117
118
119
120
121
# File 'lib/rdf/reasoner/rdfs.rb', line 116

def subClass
  raise RDF::Reasoner::Error, "#{self} Can't entail subClass" unless class?
  subClass_cache[self] ||= ([self.vocab] + self.vocab.imported_from).map do |v|
    Array(v.properties).select {|p| p.class? && Array(p.subClassOf).include?(self)}
  end.flatten.compact
end

#subClass_cacheRDF::Util::Cache

Returns:

  • (RDF::Util::Cache)


19
20
21
# File 'lib/rdf/reasoner/rdfs.rb', line 19

def subClass_cache
  @@subClass_cache_cache ||= RDF::Util::Cache.new(-1)
end

#subClassOf_cacheRDF::Util::Cache

Returns:

  • (RDF::Util::Cache)


12
13
14
# File 'lib/rdf/reasoner/rdfs.rb', line 12

def subClassOf_cache
  @@subClassOf_cache ||= RDF::Util::Cache.new(-1)
end

#subPropertyArray<RDF::Vocabulary::Term>

Get the immediate subproperties of this property.

This iterates over terms defined in the vocabulary of this term, as well as the vocabularies imported by this vocabulary.

Returns:

  • (Array<RDF::Vocabulary::Term>)

Raises:



189
190
191
192
193
194
195
196
197
198
# File 'lib/rdf/reasoner/rdfs.rb', line 189

def subProperty
  raise RDF::Reasoner::Error,
    "#{self} Can't entail subProperty" unless property?
  vocabs = [self.vocab] + self.vocab.imported_from
  subProperty_cache[self] ||= vocabs.map do |v|
    Array(v.properties).select do |p|
      p.property? && Array(p.subPropertyOf).include?(self)
    end
  end.flatten.compact
end

#subProperty_cacheRDF::Util::Cache

Returns:

  • (RDF::Util::Cache)


40
41
42
# File 'lib/rdf/reasoner/rdfs.rb', line 40

def subProperty_cache
  @@subProperty_cache ||= RDF::Util::Cache.new(-1)
end

#subPropertyOf_cacheRDF::Util::Cache

Returns:

  • (RDF::Util::Cache)


33
34
35
# File 'lib/rdf/reasoner/rdfs.rb', line 33

def subPropertyOf_cache
  @@subPropertyOf_cache ||= RDF::Util::Cache.new(-1)
end