ruby-rdf/rdf

View on GitHub
lib/rdf/vocabulary.rb

Summary

Maintainability
F
1 wk
Test Coverage
module RDF
  ##
  # A {Vocabulary} represents an RDFS or OWL vocabulary.
  #
  # A {Vocabulary} can also serve as a Domain Specific Language (DSL) for generating an RDF Graph definition for the vocabulary (see {RDF::Vocabulary#to_enum}).
  #
  # ### Defining a vocabulary using the DSL
  # Vocabularies can be defined based on {RDF::Vocabulary} or {RDF::StrictVocabulary} using a simple Domain Specific Language (DSL).
  #
  # * Ontology information for the vocabulary itself can be specified using the {ontology} method.
  # * Terms of the vocabulary are specified using either `property` or `term` (alias), with the attributes of the term listed in a hash. See {property} for description of the hash. Term attributes become properties of the associated {RDF::Vocabulary::Term} (see {RDF::Vocabulary::Term#attributes}).
  #
  # Note that, by default, the prefix associated with the vocabulary for forming and interpreting PNames is created from the class name of the vocabulary. See {\_\_prefix\_\_=} for overriding this at runtime.
  #
  # The simplest way to generate a DSL representation of a vocabulary is using {RDF::Vocabulary::Writer} given an {RDF::Graph} representation of the vocabulary.
  #
  # ### Vocabularies:
  #
  # The following vocabularies are pre-defined for your convenience:
  #
  # * {RDF}         - Resource Description Framework (RDF)
  # * {RDF::OWL}    - Web Ontology Language (OWL)
  # * {RDF::RDFS}   - RDF Schema (RDFS)
  # * {RDF::XSD}    - XML Schema (XSD)
  #
  # Other vocabularies are defined in the [rdf-vocab](https://rubygems.org/gems/rdf-vocab) gem
  #
  # @example Using pre-defined RDF vocabularies
  #   include RDF
  #
  #   RDF.type      #=> RDF::URI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")
  #   RDFS.seeAlso  #=> RDF::URI("http://www.w3.org/2000/01/rdf-schema#seeAlso")
  #   OWL.sameAs    #=> RDF::URI("http://www.w3.org/2002/07/owl#sameAs")
  #   XSD.dateTime  #=> RDF::URI("http://www.w3.org/2001/XMLSchema#dateTime")
  #
  # @example Using ad-hoc RDF vocabularies
  #   foaf = RDF::Vocabulary.new("http://xmlns.com/foaf/0.1/")
  #   foaf.knows    #=> RDF::URI("http://xmlns.com/foaf/0.1/knows")
  #   foaf[:name]   #=> RDF::URI("http://xmlns.com/foaf/0.1/name")
  #   foaf['mbox']  #=> RDF::URI("http://xmlns.com/foaf/0.1/mbox")
  #
  # @example Defining a simple vocabulary
  #   EX = Class.new(RDF::StrictVocabulay("http://example/ns#")) do
  #     # Ontology definition
  #     ontology :"http://example/ns#",
  #       label: "The RDF Example Vocablary",
  #       type: "http://www.w3.org/2002/07/owl#Ontology"
  #
  #     # Class definitions
  #     term :Class,
  #       label: "My Class",
  #       comment: "Good to use as an example",
  #       type: "rdfs:Class",
  #       subClassOf: "http://example/SuperClass",
  #       "ex:prop": "Some annotation property not having a shortcut"
  #
  #     # Property definitions
  #     property :prop,
  #       comment: "A description of the property",
  #       label: "property",
  #       domain: "http://example/ns#Class",
  #       range: "rdfs:Literal",
  #       isDefinedBy: %(ex:),
  #       type: "rdf:Property"
  #   end
  # 
  # @example Method calls are converted to the typical RDF camelcase convention
  #   foaf = RDF::Vocabulary.new("http://xmlns.com/foaf/0.1/")
  #   foaf.family_name    #=> RDF::URI("http://xmlns.com/foaf/0.1/familyName")
  #   owl.same_as         #=> RDF::URI("http://www.w3.org/2002/07/owl#sameAs")
  #   # []-style access is left as is
  #   foaf['family_name'] #=> RDF::URI("http://xmlns.com/foaf/0.1/family_name")
  #   foaf[:family_name]  #=> RDF::URI("http://xmlns.com/foaf/0.1/family_name")
  #
  # @example Generating RDF from a vocabulary definition
  #   graph = RDF::Graph.new << RDF::RDFS.to_enum
  #   graph.dump(:ntriples)
  #
  # @see https://www.w3.org/TR/rdf-sparql-query/#prefNames
  class Vocabulary
    extend ::Enumerable

    autoload :Format, 'rdf/vocab/writer'
    autoload :Writer, 'rdf/vocab/writer'

    class << self
      ##
      # Enumerates known RDF vocabulary classes.
      #
      # @yield  [klass]
      # @yieldparam [Class] klass
      # @return [Enumerator]
      def each(&block)
        if self.equal?(Vocabulary)
          if instance_variable_defined?(:@vocabs) && @vocabs
            @vocabs.select(&:name).each(&block)
          else
            # This is needed since all vocabulary classes are defined using
            # Ruby's autoloading facility, meaning that `@@subclasses` will be
            # empty until each subclass has been touched or require'd.
            RDF::VOCABS.each { |v, p| RDF.const_get(p[:class_name].to_sym) unless v == :rdf }
            @@subclasses.select(&:name).each(&block)
          end
        else
          __properties__.each(&block)
        end
      end

      ##
      # A hash of all vocabularies by prefix showing relevant URI and
      # associated vocabulary Class Name
      #
      # @return [Hash{Symbol => Hash{Symbol => String}}]
      def vocab_map
        # Create an initial duplicate of RDF::VOCABS. We want to
        # ensure the hash itself is modifiable but the values are
        # frozen.
        @vocab_map ||= RDF::VOCABS.transform_values(&:freeze)
      end

      ##
      # Return the vocabulary based on it's class_name symbol
      #
      # @param [Symbol] sym
      # @return [RDF::Vocabulary]
      def from_sym(sym)
        RDF.const_get(sym.to_sym)
      end

      ##
      # Register a vocabulary for internal prefix lookups. Parameters
      # of interest include `:uri`, `:class_name`, `:source`, and `:skip`.
      #
      # @param prefix [Symbol] the prefix to use
      # @param vocab  [String, Class] either the URI or the vocab class
      # @param params [Hash{Symbol => String}] Relevant parameters
      # @return [Hash] The parameter hash, but frozen
      def register(prefix, vocab, **params)
        # check the input
        raise ArgumentError, "#{prefix} must be symbol-able" unless
          [String, Symbol].any? { |c| prefix.is_a? c }

        # note an explicit uri: param overrides
        case vocab
        when String then params[:uri] ||= vocab
        when Class
          raise ArgumentError, 'vocab must be an RDF::(Strict)Vocabulary' unless
            vocab.ancestors.include? RDF::Vocabulary
          params[:class] = vocab
          params[:uri] ||= vocab.to_uri.to_s
        end

        # fill in the class name
        params[:class_name] ||= prefix.to_s.upcase

        # now freeze and assign
        vocab_map[prefix.to_s.to_sym] = params.freeze
      end

      ##
      # Limits iteration over vocabularies to just those selected
      #
      # @example limit to set of vocabularies by symbol
      #     RDF::Vocabulary.limit_vocabs(:rdf, :rdfs
      #     RDF::Vocabulary.find_term('http://www.w3.org/2000/01/rdf-schema#Resource').pname
      #     # => 'rdfs:Resource'
      #
      # @example limit to set of vocabularies by class name
      #     RDF::Vocabulary.limit_vocabs(RDF::RDFV, RDF::RDFS)
      #     RDF::Vocabulary.find_term('http://www.w3.org/2000/01/rdf-schema#Resource').pname
      #     # => 'rdfs:Resource'
      #
      # @param [Array<symbol, RDF::Vocabulary>] vocabs
      #   A list of vocabularies (symbols or classes) which may
      #   be returned by {Vocabulary.each}. Also limits
      #   vocabularies that will be inspeced for other methods.
      #   Set to nil, or an empty array to reset.
      # @return [Array<RDF::Vocabulary>]
      def limit_vocabs(*vocabs)
        @vocabs = if Array(vocabs).empty?
          nil
        else
          vocabs.map do |vocab|
            vocab = :rdfv if vocab == :rdf
            vocab.is_a?(Symbol) && RDF::VOCABS.key?(vocab) ?
              RDF.const_get(RDF::VOCABS[vocab][:class_name].to_sym) :
              vocab
          end.compact
        end
      end

      ##
      # Is this a strict vocabulary, or a liberal vocabulary allowing arbitrary properties?
      def strict?; false; end

      ##
      # @overload property
      #   Returns `property` in the current vocabulary
      #   @return [RDF::Vocabulary::Term]
      #
      # @overload property(name, options)
      #   Defines a new property or class in the vocabulary as a {RDF::Vocabulary::Term}.
      #
      #   @example A simple term definition
      #       property :domain,
      #         comment: %(A domain of the subject property.),
      #         domain: "rdf:Property",
      #         label: "domain",
      #         range: "rdfs:Class",
      #         isDefinedBy: %(rdfs:),
      #         type: "rdf:Property"
      #
      #   @example A term definition with tagged values
      #       property :actor,
      #       comment: {en: "Subproperty of as:attributedTo that identifies the primary actor"},
      #       domain: "https://www.w3.org/ns/activitystreams#Activity",
      #       label: {en: "actor"},
      #       range: term(
      #           type: "http://www.w3.org/2002/07/owl#Class",
      #           unionOf: list("https://www.w3.org/ns/activitystreams#Object", "https://www.w3.org/ns/activitystreams#Link")
      #         ),
      #       subPropertyOf: "https://www.w3.org/ns/activitystreams#attributedTo",
      #       type: "http://www.w3.org/2002/07/owl#ObjectProperty"
      #
      #   @example A SKOS term with anonymous values
      #         term: :af,
      #           type: "jur:Country",
      #           isDefinedBy: "http://eulersharp.sourceforge.net/2003/03swap/countries#",
      #           "skos:exactMatch": [
      #             Term.new(
      #               type: "skos:Concept",
      #               inScheme: "iso3166-1-alpha-2",
      #               notation: "ax"),
      #             Term.new(
      #               type: "skos:Concept",
      #               inScheme: "iso3166-1-alpha-3",
      #               notation: "ala")
      #           ],
      #           "foaf:name": "Aland Islands"
      #
      #   @param [String, #to_s] name
      #   @param [Hash{Symbol=>String,Array<String>}] options
      #     Any other values are expected to expands to a {URI} using built-in vocabulary prefixes. The value is a `String`, 'Hash{Symbol=>String,Array<String>}' or `Array<String,Hash{Symbol=>Array<String>}>` which is interpreted according to the `range` of the associated property and by heuristically determining the value datatype. See `attributes` argument to {Term#initialize}.
      #   @return [RDF::Vocabulary::Term]
      def property(*args)
        case args.length
        when 0
          Term.intern("#{self}property", vocab: self, attributes: {})
        else
          name = args.shift unless args.first.is_a?(Hash)
          options = args.last
          if name
            uri_str = [to_s, name.to_s].join('')
            URI.cache.delete(uri_str.to_sym)  # Clear any previous entry

            # Term attributes passed in a block for lazy evaluation. This helps to avoid load-time circular dependencies
            prop = Term.intern(uri_str, vocab: self, attributes: options || {})
            props[name.to_sym] = prop

            # If name is empty, also treat it as the ontology
            @ontology ||= prop if name.to_s.empty?

            # Define an accessor, except for problematic properties
            (class << self; self; end).send(:define_method, name) { prop } unless %w(property term hash).include?(name.to_s)
          else
            # Define the term without a name
            # Term attributes passed in a block for lazy evaluation. This helps to avoid load-time circular dependencies
            prop = Term.new(vocab: self, attributes: options)
          end
          prop
        end
      end

      # Alternate use for vocabulary terms, functionally equivalent to {#property}.
      alias_method :term, :property
      alias_method :__property__, :property

      ##
      # @overload ontology
      #   Returns the ontology definition of the current vocabulary as a term.
      #   @return [RDF::Vocabulary::Term]
      #
      # @overload ontology(name, options)
      #   Defines the vocabulary ontology.
      #
      #   @param [String, #to_s] uri
      #     The URI of the ontology.
      #   @param [Hash{Symbol => Object}] options
      #     See {property}
      #   @param [Hash{Symbol=>String,Array<String,Term>}] options
      #     Any other values are expected to expands to a {URI} using built-in vocabulary prefixes. The value is a `String`, `Array<String>` or `Array<Term>` which is interpreted according to the `range` of the associated property.
      #   @option options [String, Array<String,Term>] :type
      #     Shortcut for `rdf:type`, values are interpreted as a {Term}.
      #   @option options [String, Array<String>] :comment
      #     Shortcut for `rdfs:comment`, values are interpreted as a {Literal}.
      #   @option options [String, Array<String,Term>] :isDefinedBy
      #     Shortcut for `rdfs:isDefinedBy`, values are interpreted as a {Term}.
      #   @option options [String, Array<String>] :label
      #     Shortcut for `rdfs:label`, values are interpreted as a {Literal}.
      #   @option options [String, Array<String>] :altLabel
      #     Shortcut for `skos:altLabel`, values are interpreted as a {Literal}.
      #   @option options [String, Array<String>] :definition
      #     Shortcut for `skos:definition`, values are interpreted as a {Literal}.
      #   @option options [String, Array<String>] :editorialNote
      #     Shortcut for `skos:editorialNote`, values are interpreted as a {Literal}.
      #   @option options [String, Array<String>] :note
      #     Shortcut for `skos:note`, values are interpreted as a {Literal}.
      #   @option options [String, Array<String>] :prefLabel
      #     Shortcut for `skos:prefLabel`, values are interpreted as a {Literal}.
      #   @return [RDF::Vocabulary::Term]
      #
      # @note If the ontology URI has the vocabulary namespace URI as a prefix, it may also be defined using `#property` or `#term`
      def ontology(*args)
        case args.length
        when 0
          @ontology if instance_variable_defined?(:@ontology)
        else
          uri, options = args
          URI.cache.delete(uri.to_s.to_sym)  # Clear any previous entry
          # Term attributes passed in a block for lazy evaluation. This helps to avoid load-time circular dependencies
          @ontology = Term.intern(uri.to_s, vocab: self, attributes: options || {})

          # If the URI is the same as the vocabulary namespace, also define it as a term
          props[:""] ||= @ontology if self.to_s == uri.to_s

          @ontology
        end
      end
      alias_method :__ontology__, :ontology

      ##
      #  @return [Array<RDF::URI>] a list of properties in the current vocabulary
      def properties
        props.values
      end
      alias_method :__properties__, :properties

      ##
      # Attempt to expand a Compact IRI/PName using loaded vocabularies
      #
      # @param [String, #to_s] pname
      # The local-part of the PName will will have [reserved character escapes](https://www.w3.org/TR/turtle/#reserved) unescaped.
      # @return [Term]
      # @raise [KeyError] if pname suffix not found in identified vocabulary.
      # @raise [ArgumentError] if resulting URI is not valid
      def expand_pname(pname)
        return pname unless pname.is_a?(String) || pname.is_a?(Symbol)
        prefix, suffix = pname.to_s.split(":", 2)
        # Unescape escaped PN_ESCAPE_CHARS
        if suffix.match?(RDF::URI::PN_ESCAPES)
          suffix = suffix.gsub(RDF::URI::PN_ESCAPES) {|matched| matched[1..-1]}
        end
        if prefix == "rdf"
          RDF[suffix]
        elsif vocab_detail = RDF::Vocabulary.vocab_map[prefix.to_sym]
          vocab = vocab_detail[:class] ||
            RDF::Vocabulary.from_sym(vocab_detail[:class_name])
          suffix.to_s.empty? ? vocab.to_uri : vocab[suffix]
        else
          (RDF::Vocabulary.find_term(pname) rescue nil) || RDF::URI(pname, validate: true)
        end
      end

      ##
      # Return the Vocabulary associated with a URI. Allows the trailing '/' or '#' to be excluded
      #
      # @param [RDF::URI] uri
      # @return [Vocabulary]
      def find(uri)
        uri = RDF::URI(uri) if uri.is_a?(String)
        return nil unless uri.uri? && uri.valid?
        RDF::Vocabulary.detect do |v|
          if uri.length >= v.to_uri.length
            uri.start_with?(v.to_uri)
          else
            v.to_uri.to_s.sub(%r([/#]$), '') == uri.to_s
          end
        end
      end

      ##
      # Return the Vocabulary term associated with a URI
      #
      # @param [RDF::URI, String] uri
      #   If `uri` has is a pname in a locded vocabulary, the suffix portion of the PName will have escape characters unescaped before resolving against the vocabulary.
      # @return [Vocabulary::Term]
      def find_term(uri)
        uri = RDF::URI(uri)
        return uri if uri.is_a?(Vocabulary::Term)
        if vocab = find(uri)
          if vocab.ontology == uri
            vocab.ontology
          else
            suffix = uri.to_s[vocab.to_uri.to_s.length..-1].to_s
            vocab[suffix]
          end
        end
      end

      ##
      # Returns the URI for the term `property` in this vocabulary.
      #
      # @param  [#to_s] property
      # @return [RDF::URI]
      def [](property)
        if props.key?(property.to_sym)
          props[property.to_sym]
        else
          Term.intern([to_s, property.to_s].join(''), vocab: self, attributes: {})
        end
      end

      ##
      # List of vocabularies this vocabulary `owl:imports`
      #
      # @note the alias {__imports__} guards against `RDF::OWL.imports` returning a term, rather than an array of vocabularies
      # @return [Array<RDF::Vocabulary>]
      def imports
        return [] unless self.ontology
        @imports ||= begin
          Array(self.ontology.properties[:"http://www.w3.org/2002/07/owl#imports"]).compact
        rescue KeyError
          []
        end
      end
      alias_method :__imports__, :imports

      ##
      # List of vocabularies which import this vocabulary
      # @return [Array<RDF::Vocabulary>]
      def imported_from
        @imported_from ||= begin
          RDF::Vocabulary.select {|v| v.__imports__.include?(self)}
        end
      end

      ##
      # Returns the base URI for this vocabulary class.
      #
      # @return [RDF::URI]
      def to_uri
        RDF::URI.intern(@@uris[self].to_s)
      end

      # For IRI compatibility
      alias_method :to_iri, :to_uri

      ##
      # Return an enumerator over {RDF::Statement} defined for this vocabulary.
      # @return [RDF::Enumerable::Enumerator]
      # @see    Object#enum_for
      def enum_for(method = :each_statement, *args)
        # Ensure that enumerators are, themselves, queryable
        this = self
        Enumerable::Enumerator.new do |yielder|
          this.send(method, *args) {|*y| yielder << (y.length > 1 ? y : y.first)}
        end
      end
      alias_method :to_enum, :enum_for

      ##
      # Enumerate each statement constructed from the defined vocabulary terms
      #
      # If a property value is known to be a {URI}, or expands to a {URI}, the `object` is a URI, otherwise, it will be a {Literal}.
      #
      # @yield statement
      # @yieldparam [RDF::Statement]
      def each_statement(&block)
        props.each do |name, subject|
          subject.each_statement(&block)
        end

        # Also include the ontology, if it's not also a property
        @ontology.each_statement(&block) if self.ontology && self.ontology != self
      end

      ##
      # Returns a string representation of this vocabulary class.
      #
      # @return [String]
      def to_s
        @@uris.key?(self) ? @@uris[self].to_s : super
      end

      ##
      # Create a vocabulary from a graph or enumerable
      #
      # @param [RDF::Enumerable] graph
      # @param [URI, #to_s] url
      # @param [RDF::Vocabulary, String] class_name
      #   The class_name associated with the vocabulary, used for creating the class name of the vocabulary. This will create a new class named with a top-level constant based on `class_name`. If given an existing vocabulary, it replaces the existing definitions for that vocabulary with those from the graph.
      # @param [Array<Symbol>, Hash{Symbol => Hash}] extra
      #   Extra terms to add to the vocabulary. In the first form, it is an array of symbols, for which terms are created. In the second, it is a Hash mapping symbols to property attributes, as described in {RDF::Vocabulary.property}.
      # @return [RDF::Vocabulary] the loaded vocabulary
      def from_graph(graph, url: nil, class_name: nil, extra: nil)
        vocab = case class_name
        when RDF::Vocabulary
          class_name.instance_variable_set(:@ontology, nil)
          class_name.instance_variable_set(:@properties, nil)
          class_name
        when String
          Object.const_set(class_name, Class.new(self.create(url)))
        else
          Class.new(self.create(url))
        end

        ont_url = url.to_s.sub(%r([/#]$), '')
        term_defs = {}
        embedded_defs = {}
        graph.each do |statement|
          #next unless statement.subject.uri?
          if statement.subject.start_with?(url) || statement.subject == ont_url
            name = statement.subject.to_s[url.to_s.length..-1].to_s
            term = (term_defs[name.to_sym] ||= {})
          else
            # subject is not a URI or is not associated with the vocabulary
            term = (embedded_defs[statement.subject] ||= {})
          end

          key = Term::URI_ATTRs.fetch(statement.predicate) do
            statement.predicate.to_s.to_sym
          end

          (term[key] ||= []) << statement.object
        end

        # Create extra terms
        term_defs = case extra
        when Array
          extra.inject({}) {|memo, s| memo[s.to_sym] = {}; memo}.merge(term_defs)
        when Hash
          extra.merge(term_defs)
        else
          term_defs
        end

        # Pass over embedded_defs with anonymous references, once
        embedded_defs.each do |term, attributes|
          attributes.each do |ak, avs|
            # Turn embedded BNodes into either their Term definition or a List
            avs = [avs] unless avs.is_a?(Array)
            attributes[ak] = avs.map do |av|
              l = RDF::List.new(subject: av, graph: graph)
              if l.valid?
                RDF::List.new(subject: av) do |nl|
                  l.each do |lv|
                    nl << (embedded_defs[lv] ? Term.new(vocab: vocab, attributes: embedded_defs[lv]) : lv)
                  end
                end
              elsif av.is_a?(RDF::Node)
                Term.new(vocab: vocab, attributes: embedded_defs[av]) if embedded_defs[av]
              else
                av
              end
            end.compact
          end
        end

        term_defs.each do |term, attributes|
          # Turn embedded BNodes into either their Term definition or a List
          attributes.each do |ak, avs|
            attributes[ak] = avs.is_a?(Array) ? (avs.map do |av|
              l = RDF::List.new(subject: av, graph: graph)
              if l.valid?
                RDF::List.new(subject: av) do |nl|
                  l.each do |lv|
                    nl << (embedded_defs[lv] ? Term.new(vocab: vocab, attributes: embedded_defs[lv]) : lv)
                  end
                end
              elsif av.is_a?(RDF::Node)
                Term.new(vocab: vocab, attributes: embedded_defs[av]) if embedded_defs[av]
              else
                av
              end
            end).compact : avs
          end

          if term == :""
            vocab.__ontology__ vocab, attributes
          else
            vocab.__property__ term, attributes
          end
        end

        vocab
      end

      ##
      # Returns a developer-friendly representation of this vocabulary class.
      #
      # @return [String]
      def inspect
        if self == Vocabulary
          self.to_s
        else
          sprintf("%s(%s)", superclass.to_s, to_s)
        end
      end

      # Preserve the class name so that it can be obtained even for
      # vocabularies that define a `name` property:
      alias_method :__name__, :name

      ##
      # Returns a suggested vocabulary prefix for this vocabulary class.
      #
      # @return [Symbol]
      # @since  0.3.0
      def __prefix__
        instance_variable_defined?(:@__prefix__) ?
          @__prefix__ :
          __name__.split('::').last.downcase.to_sym
      end

      ##
      # Sets the vocabulary prefix to use for this vocabulary..
      #
      # @example Overriding a standard vocabulary prefix.
      #   RDF::Vocab::DC.__prefix__ = :dcterms
      #   RDF::Vocab::DC.title.pname #=> 'dcterms:title'
      #
      # @param [Symbol] prefix
      # @return [Symbol]
      # @since  3.2.3
      def __prefix__=(prefix)
        params = RDF::Vocabulary.vocab_map[__prefix__]
        @__prefix__ = prefix.to_sym
        RDF::Vocabulary.register(@__prefix__, self, **params)
        @__prefix__
      end

    protected
      def inherited(subclass) # @private
        unless @@uri.nil?
          @@subclasses << subclass unless %w(http://www.w3.org/1999/02/22-rdf-syntax-ns#).include?(@@uri)
          subclass.send(:private_class_method, :new)
          @@uris[subclass] = @@uri
          @@uri = nil
        end
        super
      end

      def method_missing(property, *args, &block)
        property = RDF::Vocabulary.camelize(property.to_s)
        if args.empty? && !to_s.empty?
          Term.intern([to_s, property.to_s].join(''), vocab: self, attributes: {})
        else
          super
        end
      end

      # Create a list of terms
      # @param [Array<String>] values
      #   Each value treated as a URI or PName
      # @return [RDF::List]
      def list(*values)
        RDF::List[*values.map {|v| expand_pname(v) rescue RDF::Literal(v)}]
      end
    private

      def props; @properties ||= {}; end
    end

    # Undefine all superfluous instance methods:
    undef_method(*instance_methods.
                  map(&:to_s).
                  select {|m| m.match?(/^\w+$/)}.
                  reject {|m| %w(object_id dup instance_eval inspect to_s class send public_send).include?(m) || m[0,2] == '__'}.
                  map(&:to_sym))

    ##
    # @param  [RDF::URI, String, #to_s] uri
    def initialize(uri)
      @uri = case uri
        when RDF::URI then uri.to_s
        else RDF::URI.parse(uri.to_s) ? uri.to_s : nil
      end
    end

    ##
    # Returns the URI for the term `property` in this vocabulary.
    #
    # @param  [#to_s] property
    # @return [URI]
    def [](property)
      Term.intern([to_s, property.to_s].join(''), vocab: self, attributes: {})
    end

    ##
    # Returns the base URI for this vocabulary.
    #
    # @return [URI]
    def to_uri
      RDF::URI.intern(to_s)
    end

    # For IRI compatibility
    alias_method :to_iri, :to_uri

    ##
    # Returns a string representation of this vocabulary.
    #
    # @return [String]
    def to_s
      @uri.to_s
    end

    ##
    # Returns a developer-friendly representation of this vocabulary.
    #
    # @return [String]
    def inspect
      sprintf("#<%s:%#0x(%s)>", self.class.name, __id__, to_s)
    end

  protected

    def self.create(uri) # @private
      @@uri = uri
      self
    end

    def method_missing(property, *args, &block)
      property = self.class.camelize(property.to_s)
      if %w(to_ary).include?(property.to_s)
        super
      elsif args.empty?
        self[property]
      else
        super
      end
    end

    def self.camelize(str)
      str.split('_').inject([]) do |buffer, e| 
        buffer.push(buffer.empty? ? e : e.capitalize)
      end.join
    end

  private

    @@subclasses = [::RDF] # @private
    @@uris       = {}      # @private
    @@uri        = nil     # @private

    # A Vocabulary Term is a {RDF::Resource} that can also act as an {Enumerable} to generate the RDF definition of vocabulary terms as defined within the vocabulary definition.
    #
    # Terms include {Term#attributes} where values a embedded resources, lists or other terms. This allows, for example, navigation of a concept heirarchy.
    #
    # Term attributes can also be accessed using {Term#properties} where the attribute values are transformed into different types of {RDF::Value}. Properties can be indexed by key, where a key is defined (See {Term::ATTR_URIs}), absolute URI, or PName, where the prefix is associated with a loaded vocabulary.
    module Term
      include RDF::Resource

      ##
      # Look up URIs for attribute symbols
      #
      # @return [Hash{Symbol => RDF::URI}]
      ATTR_URIs = {
        allValuesFrom:      RDF::URI("http://www.w3.org/2002/07/owl#allValuesFrom"),
        altLabel:           RDF::URI("http://www.w3.org/2004/02/skos/core#altLabel"),
        broader:            RDF::URI("http://www.w3.org/2004/02/skos/core#broader"),
        cardinality:        RDF::URI("http://www.w3.org/2002/07/owl#cardinality"),
        comment:            RDF::URI("http://www.w3.org/2000/01/rdf-schema#comment"),
        definition:         RDF::URI("http://www.w3.org/2004/02/skos/core#definition"),
        domain:             RDF::URI("http://www.w3.org/2000/01/rdf-schema#domain"),
        domainIncludes:     RDF::URI("http://schema.org/domainIncludes"),
        editorialNote:      RDF::URI("http://www.w3.org/2004/02/skos/core#editorialNote"),
        equivalentClass:    RDF::URI("http://www.w3.org/2002/07/owl#equivalentClass"),
        equivalentProperty: RDF::URI("http://www.w3.org/2002/07/owl#equivalentProperty"),
        exactMatch:         RDF::URI("http://www.w3.org/2004/02/skos/core#exactMatch"),
        hasTopConcept:      RDF::URI("http://www.w3.org/2004/02/skos/core#hasTopConcept"),
        inScheme:           RDF::URI("http://www.w3.org/2004/02/skos/core#inScheme"),
        intersectionOf:     RDF::URI("http://www.w3.org/2002/07/owl#intersectionOf"),
        inverseOf:          RDF::URI("http://www.w3.org/2002/07/owl#inverseOf"),
        isDefinedBy:        RDF::URI("http://www.w3.org/2000/01/rdf-schema#isDefinedBy"),
        label:              RDF::URI("http://www.w3.org/2000/01/rdf-schema#label"),
        maxCardinality:     RDF::URI("http://www.w3.org/2002/07/owl#maxCardinality"),
        member:             RDF::URI("http://www.w3.org/2004/02/skos/core#member"),
        minCardinality:     RDF::URI("http://www.w3.org/2002/07/owl#minCardinality"),
        narrower:           RDF::URI("http://www.w3.org/2004/02/skos/core#narrower"),
        notation:           RDF::URI("http://www.w3.org/2004/02/skos/core#notation"),
        note:               RDF::URI("http://www.w3.org/2004/02/skos/core#note"),
        onProperty:         RDF::URI("http://www.w3.org/2002/07/owl#onProperty"),
        prefLabel:          RDF::URI("http://www.w3.org/2004/02/skos/core#prefLabel"),
        range:              RDF::URI("http://www.w3.org/2000/01/rdf-schema#range"),
        rangeIncludes:      RDF::URI("http://schema.org/rangeIncludes"),
        related:            RDF::URI("http://www.w3.org/2004/02/skos/core#related"),
        someValuesFrom:     RDF::URI("http://www.w3.org/2002/07/owl#someValuesFrom"),
        subClassOf:         RDF::URI("http://www.w3.org/2000/01/rdf-schema#subClassOf"),
        subPropertyOf:      RDF::URI("http://www.w3.org/2000/01/rdf-schema#subPropertyOf"),
        type:               RDF::URI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
        unionOf:            RDF::URI("http://www.w3.org/2002/07/owl#unionOf"),
      }.freeze

      ##
      # Look up attribute symbols from URIs
      #
      # @return [Hash{RDF::URI => Symbol}]
      URI_ATTRs = ATTR_URIs.invert.freeze

      # @!attribute [r] comment
      #   `rdfs:comment` accessor
      #   @return [Literal, Array<Literal>]
      # @!attribute [r] label
      #   `rdfs:label` accessor
      #   @return [Literal]
      # @!attribute [r] type
      #   `rdf:type` accessor
      #   @return [Array<Term>]
      # @!attribute [r] subClassOf
      #   `rdfs:subClassOf` accessor
      #   @return [Array<Term>]
      # @!attribute [r] subPropertyOf
      #   `rdfs:subPropertyOf` accessor
      #   @return [Array<Term>]
      # @!attribute [r] domain
      #   `rdfs:domain` accessor
      #   @return [Array<Term>]
      # @!attribute [r] range
      #   `rdfs:range` accessor
      #   @return [Array<Term>]
      # @!attribute [r] isDefinedBy
      #   `rdfs:isDefinedBy` accessor
      #   @return [Array<Term>]

      # @!attribute [r] allValuesFrom
      #   `owl:allValuesFrom` accessor
      #   @return [Array<Term>]
      # @!attribute [r] cardinality
      #   `owl:cardinality` accessor
      #   @return [Array<Literal>]
      # @!attribute [r] equivalentClass
      #   `owl:equivalentClass` accessor
      #   @return [Array<Term>]
      # @!attribute [r] equivalentProperty
      #   `owl:equivalentProperty` accessor
      #   @return [Array<Term>]
      # @!attribute [r] intersectionOf
      #   `owl:intersectionOf` accessor
      #   @return [Array<Term>]
      # @!attribute [r] inverseOf
      #   `owl:inverseOf` accessor
      #   @return [Array<Term>]
      # @!attribute [r] maxCardinality
      #   `owl:maxCardinality` accessor
      #   @return [Array<Literal>]
      # @!attribute [r] minCardinality
      #   `owl:minCardinality` accessor
      #   @return [Array<Literal>]
      # @!attribute [r] onProperty
      #   `owl:onProperty` accessor
      #   @return [Array<Term>]
      # @!attribute [r] someValuesFrom
      #   `owl:someValuesFrom` accessor
      #   @return [Array<Term>]
      # @!attribute [r] unionOf
      #   `owl:unionOf` accessor
      #   @return [List<Term>, Array<Term>]

      # @!attribute [r] domainIncludes
      #   `schema:domainIncludes` accessor
      #   @return [Array<Term>]
      # @!attribute [r] rangeIncludes
      #   `schema:rangeIncludes` accessor
      #   @return [Array<Term>]

      # @!attribute [r] altLabel
      #   `skos:altLabel` accessor
      #   @return [Literal, Array<Literal>]
      # @!attribute [r] broader
      #   `skos:broader` accessor
      #   @return [Array<Term>]
      # @!attribute [r] definition
      #   `skos:definition` accessor
      #   @return [Literal, Array<Literal>]
      # @!attribute [r] editorialNote
      #   `skos:editorialNote` accessor
      #   @return [Literal, Array<Literal>]
      # @!attribute [r] exactMatch
      #   `skos:exactMatch` accessor
      #   @return [Array<Term>]
      # @!attribute [r] hasTopConcept
      #   `skos:hasTopConcept` accessor
      #   @return [Array<Term>]
      # @!attribute [r] inScheme
      #   `skos:inScheme` accessor
      #   @return [Array<Term>]
      # @!attribute [r] member
      #   `skos:member` accessor
      #   @return [Array<Term>]
      # @!attribute [r] narrower
      #   `skos:narrower` accessor
      #   @return [Array<Term>]
      # @!attribute [r] notation
      #   `skos:notation` accessor
      #   @return [Literal, Array<Literal>]
      # @!attribute [r] note
      #   `skos:note` accessor
      #   @return [Literal, Array<Literal>]
      # @!attribute [r] prefLabel
      #   `skos:prefLabel` accessor
      #   @return [Literal]
      # @!attribute [r] related
      #   `skos:related` accessor
      #   @return [Array<Term>]

      ##
      # Vocabulary of this term.
      #
      # @return [RDF::Vocabulary]
      attr_reader :vocab

      # Attributes of this vocabulary term, used for finding `label` and `comment` and to serialize the term back to RDF.
      #
      # Attributes are indexed by symbol. Symbols directly interpreted by a term are the accessors defined for the {RDF::Vocabulary::Term} class, also in {Term::ATTR_URIs}. Other keys are interpreted as absolute URIs or PNames for properties defined on this term.
      #
      # Symbols which are accessors may also be looked up by their associated URI.
      #
      # Values may be Strings, Hash (Map), or Terms, or an Array including a combination of these. A Hash (Map) is used to create a datatype/language map to one or more string values which represent either datatyped literals, or language-tagged literals as interpreted by {#attribute_value}.
      #
      # In general, this accessor is used internally. The {#properties} method interprets these values as {RDF::Value}.
      #
      # @note lookup by PName is DEPRECATED and will be removed in a future version.
      #
      # @example looking up term label
      #   RDF::RDFS.Literal.attributes[:label] #=> "Literal"
      #   RDF::RDFS.Literal.attributes[:"rdfs:label"] #=> "Literal"
      #   RDF::RDFS.Literal.attributes[RDF::RDFS.label] #=> "Literal"
      #   RDF::RDFS.Literal.attributes["http://www.w3.org/2000/01/rdf-schema#label"] #=> "Literal"
      #   RDF::RDFS.Literal.attributes[:"http://www.w3.org/2000/01/rdf-schema#label"] #=> "Literal"
      # @return [Hash{Symbol => String, Term, Hash{Symbol => String}, Array<String, Term, Hash{Symbol => String}>}]
      # @see #properties
      attr_reader :attributes

      ##
      # @overload new(uri, attributes:, vocab:, **options)
      #   @param  [URI, String, #to_s]    uri
      #   @param [Vocabulary] vocab Vocabulary of this term.
      #   @param [Hash{Symbol => String,Term,Hash{Symbol=>String,Array<String>},Array<String>}] attributes ({})
      #     Attributes of this vocabulary term, used for finding `label` and `comment` and to serialize the term back to RDF. See {#attributes} and {#properties} for other ways to access.
      #   @param  [Hash{Symbol => Object}] options
      #     Options from {URI#initialize}
      #
      # @overload new(attributes:, vocab:, **options)
      #   @param [Vocabulary] vocab Vocabulary of this term.
      #   @param [Hash{Symbol => String,Term,Hash{Symbol=>String,Array<String>},Array<String>}] attributes ({})
      #     Attributes of this vocabulary term, used for finding `label`,  `comment` and other term properties, and to serialize the term back to RDF. See {#attributes} and {#properties} for other ways to access.
      #   @param  [Hash{Symbol => Object}] options
      #     Options from {URI#initialize}
      def self.new(*args, vocab: nil, attributes: {}, **options)
        klass = if args.first.nil?
          RDF::Node
        elsif args.first.is_a?(Hash)
          args.unshift(nil)
          RDF::Node
        elsif args.first.to_s.start_with?("_:")
          args = args[1..-1].unshift($1)
          RDF::Node
        else RDF::URI
        end

        # Create default proc on attributes to allow lookup by different key types.
        attributes = attributes.dup if attributes.frozen?
        attributes.default_proc = -> (hash, key) do
          sym = case key
          when RDF::URI
            URI_ATTRs.fetch(key, key.to_s.to_sym)
          when String
            URI_ATTRs.fetch(RDF::URI(key), key.to_s.to_sym)
          when Symbol
            case key.to_s
            when /^https?:/
              # Lookup by associated attribute, or pname
              URI_ATTRs.fetch(RDF::URI(key.to_s), RDF::URI(key).pname.to_sym)
            when /:/
              uri = RDF::Vocabulary.expand_pname(key)
              # Lookup by associated attribute or URI
              URI_ATTRs.fetch(uri, uri.to_s.to_sym)
            end
          end
          hash.fetch(sym, nil)
        end

        term = klass.allocate.extend(Term)
        term.send(:initialize, *args)
        term.instance_variable_set(:@vocab, vocab)
        term.instance_variable_set(:@attributes, attributes)
        term
      end

      ##
      # Returns an interned `RDF::URI` instance based on the given `uri`
      # string.
      #
      # The maximum number of cached interned URI references is given by the
      # `CACHE_SIZE` constant. This value is unlimited by default, in which
      # case an interned URI object will be purged only when the last strong
      # reference to it is garbage collected (i.e., when its finalizer runs).
      #
      # Excepting special memory-limited circumstances, it should always be
      # safe and preferred to construct new URI references using
      # `RDF::URI.intern` instead of `RDF::URI.new`, since if an interned
      # object can't be returned for some reason, this method will fall back
      # to returning a freshly-allocated one.
      #
      # @param (see #initialize)
      # @return [RDF::URI] an immutable, frozen URI object
      def self.intern(str, *args, **options)
        (URI.cache[(str = str.to_s).to_sym] ||= self.new(str, *args, **options)).freeze
      end

      ##
      # Returns a duplicate copy of `self`.
      #
      # @return [RDF::URI]
      def dup
        term = super.extend(Term)
        term.instance_variable_set(:@vocab, vocab)
        term.instance_variable_set(:@attributes, attributes)
        term
      end

      ##
      # Determine if the URI is a valid according to RFC3987
      #
      # @return [Boolean] `true` or `false`
      # @since 0.3.9
      def valid?
        # Validate relative to RFC3987
        node? || RDF::URI::IRI.match?(to_s) || false
      end

      ##
      # Is this a class term?
      # @return [Boolean]
      def class?
        Array(self.type).any? {|t| t.to_s.include?('Class')}
      end

      ##
      # Is this a class term?
      # @return [Boolean]
      def property?
        Array(self.type).any? {|t| t.to_s.include?('Property')}
      end

      ##
      # Is this a class term?
      # @return [Boolean]
      def datatype?
        Array(self.type).any? {|t| t.to_s.include?('Datatype')}
      end

      ##
      # Is this a Restriction term?
      # @return [Boolean]
      def restriction?
        Array(self.type).any? {|t| t.to_s.include?('Restriction')}
      end

      ##
      # Is this neither a class, property or datatype term?
      # @return [Boolean]
      def other?
        Array(self.type).none? {|t| t.to_s.match?(/(Class|Property|Datatype|Restriction)/)}
      end

      ##
      # Enumerate attributes with values transformed into {RDF::Value} instances
      # Uses an empty hash with a default_proc which looks up values in attributes. The prevents specific attributes from being evaluated until acessed.
      #
      # Properties are indexed by symbol. Symbols directly interpreted by a term are the accessors defined for the {RDF::Vocabulary::Term} class, also in {Term::ATTR_URIs}. Other keys are interpreted as absolute URIs or PNames for properties defined on this term.
      #
      # Symbols which are accessors may also be looked up by their associated URI.
      #
      # @note lookup by PName is DEPRECATED and will be removed in a future version.
      #
      # @example looking up term label
      #   RDF::RDFS.Literal.label #=> RDF::Literal("Literal")
      #   RDF::RDFS.Literal.properties[:label] #=> RDF::Literal("Literal")
      #   RDF::RDFS.Literal.properties[:"rdfs:label"] #=> RDF::Literal("Literal")
      #   RDF::RDFS.Literal.properties[RDF::RDFS.label] #=> RDF::Literal("Literal")
      #   RDF::RDFS.Literal.properties["http://www.w3.org/2000/01/rdf-schema#label"] #=> RDF::Literal("Literal")
      #   RDF::RDFS.Literal.properties[:"http://www.w3.org/2000/01/rdf-schema#label"] #=> RDF::Literal("Literal")
      #
      # @return [Hash{Symbol => Array<RDF::Value>}]
      # @see #attribute_value
      def properties
        Hash.new {|hash, key| attribute_value(key)}
      end

      ##
      # Values of an attributes as {RDF::Value}.
      #
      # Attribute values are returned as either an {RDF::Value} or {Array<RDf::Value} if there is more than one value.
      #
      # Attribute values which are not already a {RDF::Value} (including strings and symbols) are converted by a heuristic loookup as follows:
      #
      # * An {RDF::URI} if it can be turned into a valid IRI using {RDF::Vocabulary.expand_pname}. This includes IRIs already in non-relative form.
      # * A {Hash{Symbol=>String,Array<String>}} is interpreted as a datatype/language map. If the key contains a ':', it is treated as a PName or IRI datatype applied to the values. Otherwise, it is treated as a language-tag applied to the values.
      # * {RDF::Literal::Date} if valid,
      # * {RDF::Literal::DateTime} if valid, 
      # * {RDF::Literal::Integer} if valid, 
      # * {RDF::Literal::Decimal} if valid, 
      # * {RDF::Literal::Double} if valid, 
      # * {RDF::Literal::Boolean} if valid
      # * Otherwise, {RDF::Literal} where type may be inferred by the class of the value.
      #
      # @param [Symbol] prop
      # @return [RDF::Value, Array<RDF::Value>]
      def attribute_value(prop)
        values = attributes[prop]
        return nil if values.nil?
        values = [values].compact unless values.is_a?(Array)
        prop_values = values.map do |value|
          v = value.is_a?(Symbol) ? value.to_s : value
          value = (RDF::Vocabulary.expand_pname(v) rescue nil) if v.is_a?(String) && v.include?(':')
          value = value.to_uri if value.respond_to?(:to_uri)
          value = if value.is_a?(RDF::Value) && value.valid?
            value
          elsif value.is_a?(Hash)
            # type/language map
            value.inject([]) do |memo, (k,v)|
              vv = [v] unless v.is_a?(Array)
              memo << if k.to_s.include?(':')
                dt = RDF::Vocabulary.expand_pname(v) rescue nil
                vv.map {|val| RDF::Literal(val, datatype: dt)}
              else
                vv.map {|val| RDF::Literal(val, language: k)}
              end
            end.flatten.compact.select(&:valid?)
          else
            # Use as most appropriate literal
            [
              RDF::Literal::Date,
              RDF::Literal::DateTime,
              RDF::Literal::Integer,
              RDF::Literal::Decimal,
              RDF::Literal::Double,
              RDF::Literal::Boolean,
              RDF::Literal
            ].inject(nil) do |m, klass|
              m || begin
                l = klass.new(v)
                l if l.valid?
              end
            end
          end
        end.flatten

        prop_values.length <= 1 ? prop_values.first : prop_values
      end

      ##
      # Enumerate each statement constructed from the defined vocabulary terms
      #
      # If a property value is known to be a {URI}, or expands to a {URI}, the `object` is a URI, otherwise, it will be a {Literal}.
      #
      # @yield statement
      # @yieldparam [RDF::Statement]
      def each_statement
        attributes.keys.each do |p|
          prop = ATTR_URIs.fetch(p) { RDF::Vocabulary::expand_pname(p)}
          values = attribute_value(p)
          values = [values].compact unless values.is_a?(Array)
          values.each do |value|
            yield RDF::Statement(self, prop, value) if prop.is_a?(RDF::URI)

            # Enumerate over value statements, if enumerable
            if value.is_a?(RDF::Enumerable) || (value.is_a?(Term) && value.node?)
              value.each_statement {|s| yield s}
            end
          end
        end
      end

      ##
      # Return an enumerator over {RDF::Statement} defined for this vocabulary.
      # @return [RDF::Enumerable::Enumerator]
      # @see    Object#enum_for
      def enum_for(method = :each_statement, *args)
        # Ensure that enumerators are, themselves, queryable
        this = self
        Enumerable::Enumerator.new do |yielder|
          this.send(method, *args) {|*y| yielder << (y.length > 1 ? y : y.first)}
        end
      end
      alias_method :to_enum, :enum_for

      ##
      # Returns a <code>String</code> representation of the URI object's state.
      #
      # @return [String] The URI object's state, as a <code>String</code>.
      def inspect
        sprintf("#<%s:%#0x ID:%s>", Term.to_s, self.object_id, self.to_s)
      end

      # Implement accessor to symbol attributes
      def respond_to?(method, include_all = false)
        case method
        when :comment, :notation, :note, :editorialNote, :definition,
             :label, :altLabel, :prefLabel, :type, :isDefinedBy
          true
        when :subClassOf, :subPropertyOf,
             :domainIncludes, :rangeIncludes,
             :equivalentClass, :intersectionOf, :unionOf
          self.class?
        when :domain, :range, :equivalentProperty, :inverseOf
          self.property?
        when :allValuesFrom, :cardinality,
             :maxCardinality, :minCardinality,
             :onProperty, :someValuesFrom
          self.restriction?
        when :broader, :exactMatch, :hasTopConcept, :inScheme, :member, :narrower, :related
          @attributes.key?(method)
        else
          super
        end
      end

      # Accessor for `schema:domainIncludes`
      # @return [RDF::URI]
      def domain_includes
        domainIncludes
      end

      # Accessor for `schema:rangeIncludes`
      # @return [RDF::URI]
      def range_includes
        rangeIncludes
      end

      ##
      # Serialize back to a Ruby source initializer. This is used primarily by {RDF::Vocabulary::Writer}.
      #
      # @param [String] indent
      # @return [String]
      def to_ruby(indent: "")
        "term(" +
        (self.uri? ? self.to_s.inspect + ",\n" : "\n") +
        "#{indent}  " +
        attributes.keys.sort.map do |k|
          values = attribute_value(k)
          values = [values].compact unless values.is_a?(Array)
          values = values.map do |value|
            if value.is_a?(Literal) && %w(: comment definition notation note editorialNote).include?(k.to_s)
              "%(#{value.to_s.gsub('(', '\(').gsub(')', '\)')})"
            elsif value.node? && value.is_a?(RDF::Vocabulary::Term)
              "#{value.to_ruby(indent: indent + "  ")}"
            elsif value.is_a?(RDF::Term)
              "#{value.to_s.inspect}"
            elsif value.is_a?(RDF::List)
              list_elements = value.map do |u|
                if u.uri?
                  "#{u.to_s.inspect}"
                elsif u.node? && u.respond_to?(:to_ruby)
                  u.to_ruby(indent: indent + "  ")
                else
                  "#{u.to_s.inspect}"
                end
              end
              "list(#{list_elements.join(', ')})"
            else
              "#{value.inspect}"
            end
          end
          "#{k.to_s.include?(':') ? k.to_s.inspect : k}: " +
          (values.length == 1 ? values.first : ('[' + values.join(',') + ']'))
        end.join(",\n#{indent}  ") + "\n#{indent})"

      end
    protected
      # Implement accessor to symbol attributes
      def method_missing(method, *args, &block)
        case method
        when :comment, :notation, :note, :editorialNote, :definition
          attribute_value(method)
        when :label, :altLabel, :prefLabel
          # Defaults to URI fragment or path tail
          begin
            attribute_value(method)
          rescue KeyError
            to_s.split(/[\/\#]/).last
          end
        when :type, :subClassOf, :subPropertyOf, :domain, :range, :isDefinedBy,
             :allValuesFrom, :cardinality, :equivalentClass, :equivalentProperty,
             :imports, :intersectionOf, :inverseOf, :maxCardinality, :minCardinality,
             :onProperty, :someValuesFrom, :unionOf,
             :domainIncludes, :rangeIncludes,
             :broader, :exactMatch, :hasTopConcept, :inScheme, :member, :narrower, :related

          # Return value as an Array, unless it is a list
          case value = attribute_value(method)
          when Array, RDF::List then value
          else [value].compact
          end
        else
          super
        end
      end
    end # Term
  end # Vocabulary

  # Represents an RDF Vocabulary. The difference from {RDF::Vocabulary} is that
  # that every concept in the vocabulary is required to be declared. To assist
  # in this, an existing RDF representation of the vocabulary can be loaded as
  # the basis for concepts being available
  class StrictVocabulary < Vocabulary
    class << self
      # Redefines method_missing to the original definition
      # By remaining a subclass of Vocabulary, we remain available to
      # Vocabulary::each etc.
      define_method(:method_missing, BasicObject.instance_method(:method_missing))

      ##
      # Is this a strict vocabulary, or a liberal vocabulary allowing arbitrary properties?
      def strict?; true; end

      ##
      # Returns the URI for the term `property` in this vocabulary.
      #
      # @param  [#to_s] name
      # @return [RDF::URI]
      # @raise [KeyError] if property not defined in vocabulary
      def [](name)
        props.fetch(name.to_sym)
      rescue KeyError
        raise KeyError, "#{name.inspect} not found in vocabulary #{self.__name__}"
      end
    end
  end # StrictVocabulary
end # RDF