ruby-rdf/rdf

View on GitHub
lib/rdf/model/statement.rb

Summary

Maintainability
C
1 day
Test Coverage
module RDF
  ##
  # An RDF statement.
  #
  # @example Creating an RDF statement
  #   s = RDF::URI.new("https://rubygems.org/gems/rdf")
  #   p = RDF::Vocab::DC.creator
  #   o = RDF::URI.new("http://ar.to/#self")
  #   RDF::Statement(s, p, o)
  #
  # @example Creating an RDF statement with a graph_name
  #   uri = RDF::URI("http://example/")
  #   RDF::Statement(s, p, o, graph_name: uri)
  #
  # @example Creating an RDF statement from a `Hash`
  #   RDF::Statement({
  #     subject:   RDF::URI.new("https://rubygems.org/gems/rdf"),
  #     predicate: RDF::Vocab::DC.creator,
  #     object:    RDF::URI.new("http://ar.to/#self"),
  #   })
  #
  # @example Creating an RDF statement with interned nodes
  #   RDF::Statement(:s, p, :o)
  #
  # @example Creating an RDF statement with a string
  #   RDF::Statement(s, p, "o")
  #
  class Statement
    include RDF::Resource

    ##
    # @private
    # @since 0.2.2
    def self.from(statement, graph_name: nil, **options)
      case statement
        when Array, Query::Pattern
          graph_name ||= statement[3] == false ? nil : statement[3]
          self.new(statement[0], statement[1], statement[2], graph_name: graph_name, **options)
        when Statement then statement
        when Hash      then self.new(options.merge(statement))
        else raise ArgumentError, "expected RDF::Statement, Hash, or Array, but got #{statement.inspect}"
      end
    end

    # @return [Object]
    attr_accessor :id

    # @return [RDF::Resource]
    attr_accessor :graph_name

    # @return [RDF::Resource]
    attr_accessor :subject

    # @return [RDF::URI]
    attr_accessor :predicate

    # @return [RDF::Term]
    attr_accessor :object

    # @return [Hash{Symbol => Object}]
    attr_accessor :options

    ##
    # @overload initialize(**options)
    #   @param  [Hash{Symbol => Object}] options
    #   @option options [RDF::Term]  :subject   (nil)
    #     A symbol is converted to an interned {Node}.
    #   @option options [RDF::URI]       :predicate (nil)
    #   @option options [RDF::Resource]      :object    (nil)
    #     if not a {Resource}, it is coerced to {Literal} or {Node} depending on if it is a symbol or something other than a {Term}.
    #   @option options [RDF::Term]  :graph_name   (nil)
    #     Note, in RDF 1.1, a graph name MUST be an {Resource}.
    #   @option options [Boolean] :inferred used as a marker to record that this statement was inferred based on semantic relationships (T-Box).
    #   @option options [Boolean] :quoted used as a marker to record that this statement quoted and appears as the subject or object of another RDF::Statement.
    #   @return [RDF::Statement]
    #
    # @overload initialize(subject, predicate, object, **options)
    #   @param  [RDF::Term]          subject
    #     A symbol is converted to an interned {Node}.
    #   @param  [RDF::URI]           predicate
    #   @param  [RDF::Resource]      object
    #     if not a {Resource}, it is coerced to {Literal} or {Node} depending on if it is a symbol or something other than a {Term}.
    #   @param  [Hash{Symbol => Object}] options
    #   @option options [RDF::Term]  :graph_name   (nil)
    #     Note, in RDF 1.1, a graph name MUST be an {Resource}.
    #   @option options [Boolean] :inferred used as a marker to record that this statement was inferred based on semantic relationships (T-Box).
    #   @option options [Boolean] :quoted used as a marker to record that this statement quoted and appears as the subject or object of another RDF::Statement.
    #   @return [RDF::Statement]
    def initialize(subject = nil, predicate = nil, object = nil, options = {})
      if subject.is_a?(Hash)
        @options   = Hash[subject] # faster subject.dup
        @subject   = @options.delete(:subject)
        @predicate = @options.delete(:predicate)
        @object    = @options.delete(:object)
      else
        @options   = !options.empty? ? Hash[options] : {}
        @subject   = subject
        @predicate = predicate
        @object    = object
      end
      @id          = @options.delete(:id) if @options.key?(:id)
      @graph_name  = @options.delete(:graph_name)
      initialize!
    end

    ##
    # @private
    def initialize!
      @graph_name   = Node.intern(@graph_name)   if @graph_name.is_a?(Symbol)
      @subject   = if @subject.is_a?(Value)
        @subject.to_term
      elsif @subject.is_a?(Symbol)
        Node.intern(@subject)
      elsif @subject.nil?
        nil
      else
        raise ArgumentError, "expected subject to be nil or a resource, was #{@subject.inspect}"
      end
      @predicate = Node.intern(@predicate) if @predicate.is_a?(Symbol)
      @object    = if @object.is_a?(Value)
        @object.to_term
      elsif @object.is_a?(Symbol)
        Node.intern(@object)
      elsif @object.nil?
        nil
      else
        Literal.new(@object)
      end
      @graph_name = if @graph_name.is_a?(Value)
        @graph_name.to_term
      elsif @graph_name.is_a?(Symbol)
        Node.intern(@graph_name)
      elsif !@graph_name
        @graph_name
      else
        raise ArgumentError, "expected graph_name to be nil or a resource, was #{@graph_name.inspect}"
      end
    end

    ##
    # @overload statement?
    #   Returns `true` if `self` is a {RDF::Statement}.
    #
    #   @return [Boolean]
    # @overload statement?(statement)
    #   Returns `true` if `self` contains the given {RDF::Statement}.
    #
    #   @param  [RDF::Statement] statement
    #   @return [Boolean]
    def statement?(*args)
      case args.length
      when 0 then true
      when 1 then self == args.first || subject.statement?(*args) || object.statement?(*args)
      else raise ArgumentError("wrong number of arguments (given #{args.length}, expected 0 or 1)")
      end
    end

    ##
    # @overload variable?
    #   Returns `true` if any element of the statement is not a
    # URI, Node or Literal.
    #
    #   @return [Boolean]
    # @overload variable?(variables)
    #   Returns `true` if this statement contains any of the variables.
    #
    #   @param  [Array<Symbol, #to_sym>] variables
    #   @return [Boolean]
    def variable?(*args)
      case args.length
      when 0
        !(subject?    && subject.constant? &&
          predicate?  && predicate.constant? &&
          object?     && object.constant? &&
          (graph?     ? graph_name.constant? : true))
      when 1
        to_quad.any? {|t| t.respond_to?(:variable?) && t.variable?(*args)}
      else raise ArgumentError("wrong number of arguments (given #{args.length}, expected 0 or 1)")
      end
    end

    ##
    # Returns `true` if any element of the statement is, itself, a statement.
    #
    # Note: Nomenclature is evolving, alternatives could include `#complex?` and `#nested?`
    # @return [Boolean]
    def embedded?
      subject && subject.statement? || object && object.statement?
    end

    ##
    # @return [Boolean]
    def invalid?
      !valid?
    end

    ##
    # @return [Boolean]
    def valid?
      subject?    && subject.resource? && subject.valid? &&
      predicate?  && predicate.uri? && predicate.valid? &&
      object?     && object.term? && object.valid? &&
      (graph?      ? (graph_name.resource? && graph_name.valid?) : true)
    end

    ##
    # @return [Boolean]
    def asserted?
      !quoted?
    end

    ##
    # @return [Boolean]
    def quoted?
      !!@options[:quoted]
    end

    ##
    # @return [Boolean]
    def inferred?
      !!@options[:inferred]
    end

    ##
    # Determines if the statement is incomplete, vs. invalid. An incomplete statement is one in which any of `subject`, `predicate`, or `object`, are nil.
    #
    # @return [Boolean]
    # @since 3.0
    def incomplete?
      to_triple.any?(&:nil?)
    end

    ##
    # Determines if the statement is complete, vs. invalid. A complete statement is one in which none of `subject`, `predicate`, or `object`, are nil.
    #
    # @return [Boolean]
    # @since 3.0
    def complete?
      !incomplete?
    end

    ##
    # @overload graph?
    #   Returns `true` if the statement has a graph name.
    #
    #   @return [Boolean]
    # @overload graph?(name)
    #   Returns `true` if `self` contains the given RDF graph_name.
    #
    #   @param  [RDF::Resource, false] graph_name
    #     Use value `false` to query for the default graph_name
    #   @return [Boolean]
    def graph?(*args)
      case args.length
      when 0 then !!graph_name
      when 1 then graph_name == args.first
      else raise ArgumentError("wrong number of arguments (given #{args.length}, expected 0 or 1)")
      end
    end
    alias_method :name?, :graph?
    alias_method :has_graph?, :graph?
    alias_method :has_name?, :graph?

    ##
    # @return [Boolean]
    def subject?
      !!subject
    end
    alias_method :has_subject?, :subject?

    ##
    # @return [Boolean]
    def predicate?
      !!predicate
    end
    alias_method :has_predicate?, :predicate?

    ##
    # @return [Boolean]
    def object?
      !!object
    end
    alias_method :has_object?, :object?

    ##
    # Returns `true` if any resource of this statement is a blank node
    # or has an embedded statement including a blank node.
    #
    # @return [Boolean]
    # @since 2.0
    def node?
      to_quad.compact.any?(&:node?)
    end
    alias_method :has_blank_nodes?, :node?

    ##
    # Checks statement equality as a quad.
    #
    # @param  [Statement] other
    # @return [Boolean]
    #
    # @see RDF::URI#==
    # @see RDF::Node#==
    # @see RDF::Literal#==
    # @see RDF::Query::Variable#==
    def eql?(other)
      other.is_a?(Statement) && self.to_a.eql?(other.to_a) && (self.graph_name || false) == (other.graph_name || false)
    end

    ##
    # Generates a Integer hash value as a quad.
    def hash
      @hash ||= to_quad.hash
    end

    ##
    # Checks statement equality as a triple.
    #
    # @param  [Object] other
    # @return [Boolean]
    #
    # @see RDF::URI#==
    # @see RDF::Node#==
    # @see RDF::Literal#==
    # @see RDF::Query::Variable#==
    def ==(other)
      to_a == Array(other) &&
        !(other.is_a?(RDF::Value) && other.list?)
    end

    ##
    # Checks statement equality with patterns.
    #
    # Uses `#eql?` to compare each of `#subject`, `#predicate`, `#object`, and
    # `#graph_name` to those of `other`. Any statement part which is not
    # present in `self` is ignored.
    #
    # @example
    #   statement = RDF::Statement.new(RDF::URI('s'), RDF::URI('p'), RDF::URI('o'))
    #   pattern   = RDF::Statement.new(RDF::URI('s'), RDF::URI('p'), RDF::Query::Variable.new)
    #
    #   # true
    #   statement === statement
    #   pattern   === statement
    #   RDF::Statement.new(nil, nil, nil) === statement
    #
    #   # false
    #   statement === pattern
    #   statement === RDF::Statement.new(nil, nil, nil)
    #
    # @param  [Statement] other
    # @return [Boolean]
    #
    # @see RDF::URI#eql?
    # @see RDF::Node#eql?
    # @see RDF::Literal#eql?
    # @see RDF::Query::Variable#eql?
    def ===(other)
      return false if object?    && !object.eql?(other.object)
      return false if predicate? && !predicate.eql?(other.predicate)
      return false if subject?   && !subject.eql?(other.subject)
      return false if graph?     && !graph_name.eql?(other.graph_name)
      return true
    end

    ##
    # @param  [Integer] index
    # @return [RDF::Term]
    def [](index)
      case index
        when 0 then self.subject
        when 1 then self.predicate
        when 2 then self.object
        when 3 then self.graph_name
        else nil
      end
    end

    ##
    # @param  [Integer]    index
    # @param  [RDF::Term] value
    # @return [RDF::Term]
    def []=(index, value)
      case index
        when 0 then self.subject   = value
        when 1 then self.predicate = value
        when 2 then self.object    = value
        when 3 then self.graph_name   = value
        else nil
      end
    end

    ##
    # @return [Array(RDF::Term)]
    def to_quad
      [subject, predicate, object, graph_name]
    end

    ##
    # @return [Array(RDF::Term)]
    def to_triple
      [subject, predicate, object]
    end
    alias_method :to_a, :to_triple

    ##
    # Returns an array of all the non-nil non-statement terms.
    # @return [Array(RDF::Term)]
    def terms
      to_quad.map {|t| t.respond_to?(:terms) ? t.terms : t}.flatten.compact
    end

    ##
    # Canonicalizes each unfrozen term in the statement.
    #
    # @return [RDF::Statement] `self`
    # @since  1.0.8
    # @raise [ArgumentError] if any element cannot be canonicalized.
    def canonicalize!
      self.subject.canonicalize!    if subject? && !self.subject.frozen?
      self.predicate.canonicalize!  if predicate? && !self.predicate.frozen?
      self.object.canonicalize!     if object? && !self.object.frozen?
      self.graph_name.canonicalize! if graph? && !self.graph_name.frozen?
      self.validate!
      @hash = nil
      self
    end

    ##
    # Returns a version of the statement with each position in canonical form
    #
    # @return [RDF::Statement] `self` or nil if statement cannot be canonicalized
    # @since  1.0.8
    def canonicalize
      self.dup.canonicalize!
    rescue ArgumentError
      nil
    end

    # New statement with duplicated components (other than blank nodes)
    #
    # @return [RDF::Statement]
    def dup
      options = Hash[@options]
      options[:subject] = subject.is_a?(RDF::Node) ? subject : subject.dup
      options[:predicate] = predicate.dup
      options[:object] = object.is_a?(RDF::Node) ? object : object.dup
      options[:graph_name] = graph_name.is_a?(RDF::Node) ? graph_name : graph_name.dup if graph_name
      RDF::Statement.new(options)
    end

    ##
    # Returns the terms of this statement as a `Hash`.
    #
    # @param  [Symbol] subject_key
    # @param  [Symbol] predicate_key
    # @param  [Symbol] object_key
    # @return [Hash{Symbol => RDF::Term}]
    def to_h(subject_key = :subject, predicate_key = :predicate, object_key = :object, graph_key = :graph_name)
      {subject_key => subject, predicate_key => predicate, object_key => object, graph_key => graph_name}
    end

    ##
    # Returns a string representation of this statement.
    #
    # @return [String]
    def to_s
      (graph_name ? to_quad : to_triple).map do |term|
        if term.is_a?(Statement)
          "<<#{term.to_s[0..-3]}>>"
        elsif term.respond_to?(:to_base)
          term.to_base
        else
          term.inspect
        end
      end.join(" ") + " ."
    end

    ##
    # Returns a graph containing this statement in reified form.
    #
    # @param [RDF::Term]  subject   (nil)
    #   Subject of reification.
    # @param [RDF::Term]  id   (nil)
    #   Node identifier, when subject is anonymous
    # @param [RDF::Term]  graph_name   (nil)
    #   Note, in RDF 1.1, a graph name MUST be an {Resource}.
    # @return [RDF::Graph]
    # @see    http://www.w3.org/TR/rdf-primer/#reification
    def reified(subject: nil, id: nil, graph_name: nil)
      RDF::Graph.new(graph_name: graph_name) do |graph|
        subject = subject || RDF::Node.new(id)
        graph << [subject, RDF.type,      RDF[:Statement]]
        graph << [subject, RDF.subject,   self.subject]
        graph << [subject, RDF.predicate, self.predicate]
        graph << [subject, RDF.object,    self.object]
      end
    end
  end
end