activefx/criterion

View on GitHub
lib/criterion.rb

Summary

Maintainability
B
4 hrs
Test Coverage
require "criterion/version"
require "forwardable"

module Criterion
  extend Forwardable

  def_delegators :criteria,
    :where, :or, :not, :order, :limit, :offset, :skip,
    :sum, :maximum, :minimum, :average

  def criteria
    Criteria.new(self)
  end

  class Criteria
    extend Forwardable
    include Enumerable

    MULTI_VALUE_METHODS   = [ :where, :or, :not, :order ]
    SINGLE_VALUE_METHODS  = [ :limit, :offset ]
    RESULT_METHODS = [
      :[], :at, :count, :empty?, :fetch, :first, :include?, :index,
      :last, :length, :reverse, :rindex, :sample, :size, :sort,
      :sort_by, :take, :take_while, :values_at
    ]

    attr_accessor \
      :where_values, :or_values, :not_values, :order_values,
      :limit_value, :offset_value

    def_delegators :to_a, *RESULT_METHODS

    def initialize(records)
      @records = records
      MULTI_VALUE_METHODS.each { |v| instance_variable_set(:"@#{v}_values", {}) }
      SINGLE_VALUE_METHODS.each { |v| instance_variable_set(:"@#{v}_value", nil) }
    end

    def where(query = {})
      clone.tap do |r|
        r.where_values.merge!(query) unless query.empty?
      end
    end

    def or(query = {})
      clone.tap do |r|
        r.or_values.merge!(query) unless query.empty?
      end
    end

    def not(query = {})
      clone.tap do |r|
        r.not_values.merge!(query) unless query.empty?
      end
    end

    def order(*args)
      sort = {}
      args.collect do |arg|
        sort.merge!(arg.is_a?(Hash) ? arg : { arg => :asc })
      end
      clone.tap do |r|
        r.order_values.merge!(sort) unless sort.empty?
      end
    end

    def limit(value = true)
      clone.tap { |r| r.limit_value = value }
    end

    def offset(value = true)
      clone.tap { |r| r.offset_value = value }
    end
    alias_method :skip, :offset

    def average(field)
      total = count
      return nil if total.zero?
      sum(field) / total.to_f
    end

    def sum(field)
      to_a.inject(0) { |sum, obj| sum + obj.send(field) }
    end

    def minimum(field)
      to_a.collect { |x| x.send(field) }.min
    end

    def maximum(field)
      to_a.collect { |x| x.send(field) }.max
    end

    def where?
      !where_values.empty?
    end

    def or?
      !or_values.empty?
    end

    def not?
      !not_values.empty?
    end

    def order?
      !order_values.empty?
    end

    def offset?
      valid_number?(offset_value)
    end

    def limit?
      valid_number?(limit_value)
    end

    def to_a
      results = @records.select{ |record| keep?(record) }
      results = results.sort_by(&ordering_args) if order?
      results = results.drop(offset_value) if offset?
      results = results.take(limit_value) if limit?
      results
    end
    alias_method :all, :to_a
    alias_method :to_ary, :to_a

    def each(&block)
      to_a.each(&block)
    end

    private

    def criteria_matches?(record, values)
      values.all? do |method, value|
        value === record.send(method)
      end
    end

    def keep?(record)
      keep = where? ? criteria_matches?(record, where_values) : true
      alt = or? ? criteria_matches?(record, or_values) : false
      exclude = not? ? criteria_matches?(record, not_values) : false
      (keep || alt) && !exclude
    end

    def ordering_args
      Proc.new do |item|
        order_values.map do |sort|
          next unless [ :asc, :desc ].include?(sort.last)
          sort.last == :desc ? -item.send(sort.first) : item.send(sort.first)
        end
      end
    end

    def valid_number?(value)
      return false unless value.is_a?(Integer)
      value >= 0
    end

  end

end