rom-rb/rom-relation

View on GitHub
lib/rom/relation.rb

Summary

Maintainability
A
0 mins
Test Coverage
# encoding: utf-8

module ROM

  # Enhanced ROM relation wrapping axiom relation and using injected mapper to
  # load/dump tuples/objects
  #
  # @example
  #
  #   # set up an axiom relation
  #   header = [[:id, Integer], [:name, String]]
  #   data   = [[1, 'John'], [2, 'Jane']]
  #   axiom  = Axiom::Relation.new(header, data)
  #
  #   # provide a simple mapper
  #   class Mapper < Struct.new(:header)
  #     def load(tuple)
  #       data = header.map { |attribute|
  #         [attribute.name, tuple[attribute.name]]
  #       }
  #       Hash[data]
  #     end
  #
  #     def dump(hash)
  #       header.each_with_object([]) { |attribute, tuple|
  #         tuple << hash[attribute.name]
  #       }
  #     end
  #   end
  #
  #   # wrap axiom relation with ROM relation
  #   mapper   = Mapper.new(axiom.header)
  #   relation = ROM::Relation.new(axiom, mapper)
  #
  #   # relation is an enumerable and it uses mapper to load/dump tuples/objects
  #   relation.to_a
  #   # => [{:id=>1, :name=>'John'}, {:id=>2, :name=>'Jane'}]
  #
  #   # you can insert/update/delete objects
  #   relation.insert(id: 3, name: 'Piotr').to_a
  #   # => [{:id=>1, :name=>"John"}, {:id=>2, :name=>"Jane"}, {:id=>3, :name=>"Piotr"}]
  #
  #   relation.delete(id: 1, name: 'John').to_a
  #   # => [{:id=>2, :name=>"Jane"}]
  #
  class Relation
    include Enumerable
    include Charlatan.new(:relation, :kind => Axiom::Relation)

    undef_method :sort_by

    attr_reader :mapper

    def initialize(relation, mapper)
      super(relation, mapper)
      @mapper = mapper
    end

    # Build a new relation
    #
    # @param [Axiom::Relation]
    # @param [Object] mapper
    #
    # @return [Relation]
    #
    # @api public
    def self.build(relation, mapper)
      new(mapper.call(relation).optimize, mapper)
    end

    # Iterate over tuples yielded by the wrapped relation
    #
    # @example
    #   mapper = Class.new {
    #     def load(value)
    #       value.to_s
    #     end
    #
    #     def dump(value)
    #       value.to_i
    #     end
    #   }.new
    #
    #   relation = ROM::Relation.new([1, 2, 3], mapper)
    #
    #   relation.each do |value|
    #     puts value # => '1'
    #   end
    #
    # @yieldparam [Object]
    #
    # @return [Relation]
    #
    # @api public
    def each
      return to_enum unless block_given?
      relation.each { |tuple| yield(mapper.load(tuple)) }
      self
    end

    # Insert an object into relation
    #
    # @example
    #   axiom    = Axiom::Relation.new([[:id, Integer]], [[1], [2]])
    #   relation = ROM::Relation.new(axiom, mapper)
    #
    #   relation.insert(id: 3)
    #   relation.to_a # => [[1], [2], [3]]
    #
    # @param [Object]
    #
    # @return [Relation]
    #
    # @api public
    def insert(object)
      new(relation.insert([mapper.dump(object)]))
    end
    alias_method :<<, :insert

    # Update an object
    #
    # @example
    #   data     = [[1, 'John'], [2, 'Jane']]
    #   axiom    = Axiom::Relation.new([[:id, Integer], [:name, String]], data)
    #   relation = ROM::Relation.new(axiom, mapper)
    #
    #   relation.update({id: 2, name: 'Jane Doe'}, {id:2, name: 'Jane'})
    #   relation.to_a # => [[1, 'John'], [2, 'Jane Doe']]
    #
    # @param [Object]
    # @param [Hash] original attributes
    #
    # @return [Relation]
    #
    # @api public
    def update(object, original_tuple)
      new(relation.delete([original_tuple]).insert([mapper.dump(object)]))
    end

    # Delete an object from the relation
    #
    # @example
    #   axiom    = Axiom::Relation.new([[:id, Integer]], [[1], [2]])
    #   relation = ROM::Relation.new(axiom, mapper)
    #
    #   relation.delete(id: 1)
    #   relation.to_a # => [[2]]
    #
    # @param [Object]
    #
    # @return [Relation]
    #
    # @api public
    def delete(object)
      new(relation.delete([mapper.dump(object)]))
    end

    # Replace all objects in the relation with new ones
    #
    # @example
    #   axiom    = Axiom::Relation.new([[:id, Integer]], [[1], [2]])
    #   relation = ROM::Relation.new(axiom, mapper)
    #
    #   relation.replace([{id: 3}, {id: 4}])
    #   relation.to_a # => [[3], [4]]
    #
    # @param [Array<Object>]
    #
    # @return [Relation]
    #
    # @api public
    def replace(objects)
      new(relation.replace(objects.map(&mapper.method(:dump))))
    end

    # Take objects form the relation with provided limit
    #
    # @example
    #   axiom    = Axiom::Relation.new([[:id, Integer]], [[1], [2]])
    #   relation = ROM::Relation.new(axiom, mapper)
    #
    #   relation.take(2).to_a # => [[2]]
    #
    # @param [Integer] limit
    #
    # @return [Relation]
    #
    # @api public
    def take(limit)
      new(sorted.take(limit))
    end

    # Take first n-objects from the relation
    #
    # @example
    #   axiom    = Axiom::Relation.new([[:id, Integer]], [[1], [2]])
    #   relation = ROM::Relation.new(axiom, mapper)
    #
    #   relation.first.to_a # => [[1]]
    #   relation.first(2).to_a # => [[1], [2]]
    #
    # @param [Integer]
    #
    # @return [Relation]
    #
    # @api public
    def first(limit = 1)
      new(sorted.first(limit))
    end

    # Take last n-objects from the relation
    #
    # @example
    #   axiom    = Axiom::Relation.new([[:id, Integer]], [[1], [2]])
    #   relation = ROM::Relation.new(axiom, mapper)
    #
    #   relation.last.to_a # => [[2]]
    #   relation.last(2).to_a # => [[1], [2]]
    #
    # @param [Integer] limit
    #
    # @return [Relation]
    #
    # @api public
    def last(limit = 1)
      new(sorted.last(limit))
    end

    # Drop objects from the relation by the given offset
    #
    # @example
    #   axiom    = Axiom::Relation.new([[:id, Integer]], [[1], [2]])
    #   relation = ROM::Relation.new(axiom, mapper)
    #
    #   relation.drop(1).to_a # => [[2]]
    #
    # @param [Integer]
    #
    # @return [Relation]
    #
    # @api public
    def drop(offset)
      new(sorted.drop(offset))
    end

    # Return exactly one object matching criteria or raise an error
    #
    # @example
    #   axiom    = Axiom::Relation.new([[:id, Integer]], [1]])
    #   relation = ROM::Relation.new(axiom, mapper)
    #
    #   relation.one.to_a # => {id: 1}
    #
    # @param [Proc] block
    #   optional block to call in case no tuple is returned
    #
    # @return [Object]
    #
    # @raise NoTuplesError
    #   if no tuples were returned
    #
    # @raise ManyTuplesError
    #   if more than one tuple was returned
    #
    # @api public
    def one(&block)
      block  ||= ->() { raise NoTuplesError }
      tuples   = take(2).to_a

      if tuples.count > 1
        raise ManyTuplesError
      else
        tuples.first || block.call
      end
    end

    # Inject a new mapper into this relation
    #
    # @example
    #
    #   relation = ROM::Relation.new([], mapper)
    #   relation.inject_mapper(new_mapper)
    #
    # @param [Object] a mapper object
    #
    # @return [Relation]
    #
    # @api public
    def inject_mapper(mapper)
      new(relation, mapper)
    end

    private

    # Sort wrapped relation using all attributes in the header
    #
    # @return [Axiom::Relation]
    #
    # @api private
    def sorted
      relation.sort
    end

    # Return new relation instance
    #
    # @return [Relation]
    #
    # @api private
    def new(new_relation, new_mapper = mapper)
      self.class.new(new_relation, new_mapper)
    end

  end # class Relation

end # module ROM