ruby-rdf/rdf

View on GitHub
lib/rdf/query/solution.rb

Summary

Maintainability
B
4 hrs
Test Coverage
class RDF::Query
  ##
  # An RDF query solution.
  #
  # @example Iterating over every binding in the solution
  #   solution.each_binding  { |name, value| puts value.inspect }
  #   solution.each_variable { |variable| puts variable.value.inspect }
  #
  # @example Iterating over every value in the solution
  #   solution.each_value    { |value| puts value.inspect }
  #
  # @example Checking whether a variable is bound or unbound
  #   solution.bound?(:title)
  #   solution.unbound?(:mbox)
  #
  # @example Retrieving the value of a bound variable
  #   solution[:mbox]
  #   solution.mbox
  #
  # @example Retrieving all bindings in the solution as a `Hash`
  #   solution.to_h       #=> {mbox: "jrhacker@example.org", ...}
  #
  class Solution
    # Undefine all superfluous instance methods:
    alias_method :__send, :send

    # Temporarily remember instance method for deprecation message in `method_missing`.
    INSTANCE_METHODS = 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 private_methods public_methods class method pretty_print).include?(m) || m[0,2] == '__'}.
                  map(&:to_sym))

    include Enumerable

    ##
    # Initializes the query solution.
    #
    # @param  [Hash{Symbol => RDF::Term}] bindings
    # @yield  [solution]
    def initialize(bindings = {}, &block)
      @bindings = bindings.to_h

      if block_given?
        case block.arity
          when 1 then block.call(self)
          else instance_eval(&block)
        end
      end
    end

    # @private
    attr_reader :bindings

    ##
    # Enumerates over every variable binding in this solution.
    #
    # @yield  [name, value]
    # @yieldparam [Symbol] name
    # @yieldparam [RDF::Term] value
    # @return [Enumerator]
    def each_binding(&block)
      @bindings.each(&block) if block_given?
      enum_binding
    end
    alias_method :each, :each_binding

    ##
    # Returns an enumerator for {#each_binding}.
    #
    # @return [Enumerator<RDF::Resource>]
    # @see    #each_subject
    def enum_binding
      enum_for(:each_binding)
    end

    ##
    # Enumerates over every variable name in this solution.
    #
    # @yield  [name]
    # @yieldparam [Symbol] name
    # @return [Enumerator]
    def each_name(&block)
      @bindings.each_key(&block) if block_given?
      enum_name
    end
    alias_method :each_key, :each_name

    ##
    # Returns an enumerator for {#each_name}.
    #
    # @return [Enumerator<RDF::Resource>]
    # @see    #each_subject
    def enum_name
      enum_for(:each_name)
    end

    ##
    # Enumerates over every variable value in this solution.
    #
    # @yield  [value]
    # @yieldparam [RDF::Term] value
    # @return [Enumerator]
    def each_value(&block)
      @bindings.each_value(&block) if block_given?
      enum_value
    end

    ##
    # Returns an enumerator for {#each_value}.
    #
    # @return [Enumerator<RDF::Resource>]
    # @see    #each_subject
    def enum_value
      enum_for(:each_value)
    end

    ##
    # @overload variable?
    #   Returns `false`.
    #
    #   @return [Boolean]
    # @overload variable?(variables)
    #   Returns `true` if this solution contains bindings for any of the given
    # `variables`.
    #
    #   @param  [Array<Symbol, #to_sym>] variables
    #   @return [Boolean]
    # @since  0.3.0
    def variable?(*args)
      case args.length
      when 0 then false
      when 1
        args.first.any? { |variable| bound?(variable) }
      else raise ArgumentError("wrong number of arguments (given #{args.length}, expected 0 or 1)")
      end
    end
    alias_method :variables?, :variable?
    alias_method :has_variables?, :variable?

    ##
    # Enumerates over every variable in this solution.
    #
    # @yield  [variable]
    # @yieldparam [Variable]
    # @return [Enumerator]
    def each_variable
      if block_given?
        @bindings.each do |name, value|
          yield Variable.new(name, value)
        end
      end
      enum_variable
    end

    ##
    # Returns an enumerator for {#each_variable}.
    #
    # @return [Enumerator<RDF::Resource>]
    # @see    #each_subject
    def enum_variable
      enum_for(:each_variable)
    end

    ##
    # Returns `true` if the variable `name` is bound in this solution.
    #
    # @param  [Symbol, #to_sym] name
    #   the variable name
    # @return [Boolean] `true` or `false`
    def bound?(name)
      !unbound?(name)
    end

    ##
    # Returns `true` if the variable `name` is unbound in this solution.
    #
    # @param  [Symbol, #to_sym] name
    #   the variable name
    # @return [Boolean] `true` or `false`
    def unbound?(name)
      @bindings[name.to_sym].nil?
    end

    ##
    # Returns the value of the variable `name`.
    #
    # @param  [Symbol, #to_sym] name
    #   the variable name
    # @return [RDF::Term]
    def [](name)
      @bindings[name.to_sym]
    end

    ##
    # Binds or rebinds the variable `name` to the given `value`.
    #
    # @param  [Symbol, #to_sym] name
    #   the variable name
    # @param  [RDF::Term] value
    # @return [RDF::Term]
    # @since  0.3.0
    def []=(name, value)
      @bindings[name.to_sym] = value.is_a?(RDF::Term) ? value : RDF::Literal(value)
    end

    ##
    # Merges the bindings from the given `other` query solution into this
    # one, overwriting any existing ones having the same name.
    #
    # ## RDF-star
    #
    # If merging a binding for a statement to a pattern,
    # merge their embedded solutions.
    #
    # @param  [RDF::Query::Solution, #to_h] other
    #   another query solution or hash bindings
    # @return [void] self
    # @since  0.3.0
    def merge!(other)
      @bindings.merge!(other.to_h) do |key, v1, v2|
        # Don't merge a pattern over a statement
        # This happens because JOIN does a reverse merge,
        # and a pattern is set in v2.
        v2.is_a?(Pattern) ? v1 : v2
      end
      # Merge bindings from patterns
      embedded_solutions = []
      @bindings.each do |k, v|
        if v.is_a?(Pattern) && other[k].is_a?(RDF::Statement)
          embedded_solutions << v.solution(other[k])
        end
      end
      # Merge embedded solutions
      embedded_solutions.each {|soln| merge!(soln)}
      self
    end

    ##
    # Merges the bindings from the given `other` query solution with a copy
    # of this one.
    #
    # @param  [RDF::Query::Solution, #to_h] other
    #   another query solution or hash bindings
    # @return [RDF::Query::Solution]
    # @since  0.3.0
    def merge(other)
      self.class.new(@bindings.dup).merge!(other)
    end

    ##
    # Duplicate solution, preserving patterns
    # @return [RDF::Statement]
    def dup
      merge({})
    end

    ##
    # Compatible Mappings
    #
    # Two solution mappings u1 and u2 are compatible if, for every variable v in dom(u1) and in dom(u2), u1(v) = u2(v).
    #
    # @param [RDF::Query::Solution, #to_h] other
    #   another query solution or hash bindings
    # @return [Boolean]
    # @see http://www.w3.org/TR/2013/REC-sparql11-query-20130321/#defn_algCompatibleMapping
    def compatible?(other)
      @bindings.all? do |k, v|
        !other.to_h.key?(k) || other[k].eql?(v)
      end
    end

    ##
    # Disjoint mapping
    #
    # A solution is disjoint with another solution if it shares no common variables in their domains.
    #
    # @param [RDF::Query::Solution] other
    # @return [Boolean]
    # @see http://www.w3.org/TR/2013/REC-sparql11-query-20130321/#defn_algMinus
    def disjoint?(other)
      @bindings.none? do |k, v|
        v && other.to_h.key?(k) && other[k].eql?(v)
      end
    end

    ##
    # Isomorphic Mappings
    # Two solution mappings u1 and u2 are isomorphic if,
    # for every variable v in dom(u1) and in dom(u2), u1(v) = u2(v).
    #
    # @param [RDF::Query::Solution, #to_h] other
    #   another query solution or hash bindings
    # @return [Boolean]
    def isomorphic_with?(other)
      @bindings.all? do |k, v|
        !other.to_h.key?(k) || other[k].eql?(v)
      end
    end
    
    ##
    # @return [Array<Array(Symbol, RDF::Term)>}
    def to_a
      @bindings.to_a
    end

    ##
    # @return [Hash{Symbol => RDF::Term}}
    def to_h
      @bindings.dup
    end
    
    ##
    # Integer hash of this solution
    # @return [Integer]
    def hash
      @bindings.hash
    end

    ##
    # Equivalence of solution
    def eql?(other)
      other.is_a?(Solution) && @bindings.eql?(other.bindings)
    end

    ##
    # Equals of solution
    def ==(other)
      other.is_a?(Solution) && @bindings == other.bindings
    end

    ##
    # @return [String]
    def inspect
      sprintf("#<%s:%#0x(%s)>", self.class.name, __id__, @bindings.inspect)
    end

  protected

    ##
    # @overload binding(name)
    #   Return the binding for this name
    #
    #   @param  [Symbol] name
    #   @return [RDF::Term]
    def method_missing(name, *args, &block)
      if args.empty? && @bindings.key?(name.to_sym)
        if INSTANCE_METHODS.include?(name)
          warn "[DEPRECATION] RDF::Query::Solution##{name} is an overridden instance method.\n" +
               "Its use as a solution accessor is deprecated and will be removed in a future version.\n" +
               "Use #[] for safe access.\n" +
               "Called from #{Gem.location_of_caller.join(':')}"
        end
        @bindings[name.to_sym]
      else
        super # raises NoMethodError
      end
    end

    ##
    # @return [Boolean]
    def respond_to_missing?(name, include_private = false)
      @bindings.key?(name.to_sym) || super
    end

    ##
    # @private
    # @param  [Symbol, #to_sym] method
    # @return [Enumerator]
    # @see    Object#enum_for
    def enum_for(method = :each)
      # Ensure that enumerators are, themselves, queryable
      this = self
      Enumerator.new do |yielder|
        this.__send(method) {|*y| yielder << (y.length > 1 ? y : y.first)}
      end
    end
    alias_method :to_enum, :enum_for
  end # Solution
end # RDF::Query