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


302
303
304
305
306
307
308
# File 'lib/rdf/reasoner/rdfs.rb', line 302

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


146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/rdf/reasoner/rdfs.rb', line 146

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


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

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


78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/rdf/reasoner/rdfs.rb', line 78

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


42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/rdf/reasoner/rdfs.rb', line 42

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_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 statement


113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/rdf/reasoner/rdfs.rb', line 113

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

#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:


194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/rdf/reasoner/rdfs.rb', line 194

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:


221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/rdf/reasoner/rdfs.rb', line 221

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?(RDF::Vocab::OGC)
          case range
          when RDF::Vocab::OGC.boolean_str
            [RDF::Vocab::OGC.boolean_str, RDF::XSD.boolean].include?(resource.datatype) ||
            resource.plain? && RDF::Literal::Boolean.new(resource.value).valid?
          when RDF::Vocab::OGC.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::Vocab::OGC.determiner_str
            # The lexical space: "", "the", "a", "an", and "auto".
            resource.plain? && (%w(the a an auto) + [""]).include?(resource.value)
          when RDF::Vocab::OGC.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::Vocab::OGC.float_str, RDF::Literal::Double, RDF::Literal::Float].include?(resource.datatype) ||
            resource.plain? && RDF::Literal::Double.new(resource.value).valid?
          when RDF::Vocab::OGC.integer_str
            resource.is_a?(RDF::Literal::Integer) ||
            [RDF::Vocab::OGC.integer_str].include?(resource.datatype) ||
            resource.plain? && RDF::Literal::Integer.new(resource.value).valid?
          when RDF::Vocab::OGC.mime_type_str
            # Valid mime type strings \(e.g., "application/mp3"\).
            [RDF::Vocab::OGC.mime_type_str].include?(resource.datatype) ||
            resource.plain? && resource.value =~ %r(^[\w\-\+]+/[\w\-\+]+$)
          when RDF::Vocab::OGC.string
            resource.plain?
          when RDF::Vocab::OGC.url
            # A string of Unicode characters forming a valid URL having the http or https scheme.
            u = RDF::URI(resource.value)
            resource.datatype == RDF::Vocab::OGC.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:


102
103
104
105
106
107
# File 'lib/rdf/reasoner/rdfs.rb', line 102

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

#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