deepcerulean/passive_record

View on GitHub
lib/passive_record/core/query.rb

Summary

Maintainability
A
3 hrs
Test Coverage
module PassiveRecord
  module Core
    class Query
      include Enumerable
      extend Forwardable
      include PassiveRecord::ArithmeticHelpers

      attr_reader :conditions

      def initialize(klass,conditions={},scope=nil)
        @klass = klass
        @conditions = conditions
        @scope = scope
      end

      def not(new_conditions={})
        NegatedQuery.new(@klass, new_conditions)
      end

      def or(query=nil)
        DisjoinedQuery.new(@klass, self, query)
      end

      def all
        if @scope
          matching = @scope.method(:matching_instances)
          if negated?
            raw_all.reject(&matching)
          else
            raw_all.select(&matching)
          end
        else
          matching = method(:matching_instances)
          raw_all.select(&matching)
        end
      end
      def_delegators :all, :sample, :uniq, :count

      def raw_all
        @klass.all
      end

      def each
        if @scope
          matching = @scope.method(:matching_instances)
          if negated?
            raw_all.each do |instance|
              yield instance unless matching[instance]
            end
          else
            raw_all.each do |instance|
              yield instance if matching[instance]
            end
          end
        else
          matching = method(:matching_instances)
          @klass.all.each do |instance|
            yield instance if matching[instance]
          end
        end
      end

      def matching_instances(instance)
        @conditions.all? do |(field,value)|
          evaluate_condition(instance, field, value)
        end
      end

      def create(attrs={})
        @klass.create(@conditions.merge(attrs))
      end

      def first_or_create(*args)
        q = where(*args)
        q.first || q.create
      end

      def where(new_conditions={})
        @conditions.merge!(new_conditions)
        self
      end

      def negated?
        false
      end

      def disjoined?
        false
      end

      def conjoined?
        false
      end

      def basic?
        !negated? && !disjoined? && !conjoined?
      end

      def and(scope_query)
        ConjoinedQuery.new(@klass, self, scope_query)
      end

      def method_missing(meth,*args,&blk)
        if @klass.methods.include?(meth)
          scope_query = @klass.send(meth,*args,&blk)
          if negated? && @scope.nil? && @conditions.empty?
            @scope = scope_query
            self
          elsif basic? && scope_query.basic?
            @conditions.merge!(scope_query.conditions)
            self
          else
            scope_query.and(self)
          end
        else
          super(meth,*args,&blk)
        end
      end

      protected
      def evaluate_condition(instance, field, value)
        case value
        when Hash  then evaluate_nested_conditions(instance, field, value)
        when Range then value.cover?(instance.send(field))
        when Array then value.include?(instance.send(field))
        else
          instance.send(field) == value
        end
      end

      def evaluate_nested_conditions(instance, field, value)
        association = instance.send(field)
        association && value.all? do |(association_field,val)|
          if association.is_a?(Associations::Relation) && !association.singular?
            association.where(association_field => val).any?
          elsif val.is_a?(Hash)
            evaluate_nested_conditions(association, association_field, val)
          else
            association.send(association_field) == val
          end
        end
      end
    end

    class NegatedQuery < Query
      def matching_instances(instance)
        @conditions.none? do |(field,value)|
          evaluate_condition(instance, field, value)
        end
      end

      def negated?
        true
      end
    end

    class DisjoinedQuery < Query
      def initialize(klass, first_query, second_query, conditions={})
        @klass = klass
        @first_query = first_query
        @second_query = second_query
        @conditions = conditions
      end

      def all
        (@first_query.where(conditions).all + @second_query.where(conditions).all).uniq
      end

      def disjoined?
        true
      end
    end

    class ConjoinedQuery < Query
      def initialize(klass, first_query, second_query, conditions={})
        @klass = klass
        @first_query = first_query
        @second_query = second_query
        @conditions = conditions
      end

      def all
        @first_query.where(conditions).all & @second_query.all
      end

      def conjoined?
        true
      end
    end

    class HasManyThroughQuery < Query
      def initialize(klass, instance, target_name_sym, conditions={})
        @klass = klass
        @instance = instance
        @target_name_sym = target_name_sym
        @conditions = conditions
      end

      def raw_all
        @instance.send(@target_name_sym).all
      end
    end
  end
end