lib/where_lower/scope_spawner.rb
module WhereLower
# :nodoc:
class ScopeSpawner
# :nodoc:
attr_reader :scope, :column_or_table_name, :value, :prefix
# Spawn different scopes based on value
# Just delegates to new though
#
# @param args [Array] arguments that are passed to #initialize
#
# @see #initialize
def self.spawn(*args)
new(*args).spawn
end
# Assign ivar only
# Actual operation is in #spawn
#
# @param scope [ActiveRecord::Relation]
# Relation or collection proxy or some AR classes
# @param column_or_table_name [Symbol, String]
# when column name, the actual column name (not attribute name)
# when table name, the actual table name (not class name converted to underscore)
# @param value [Object]
# value to compare to, also determine what scope spawner to be used
# @param prefix [Symbol, String]
# used internally for specifying a scope with prefix (table name)
#
# @see #spawn
def initialize(scope, column_or_table_name, value, prefix = nil)
@scope = scope
@column_or_table_name = column_or_table_name
@value = value
@prefix = prefix
end
# Spawn different scopes based on value
# Data conversion and query string generation are handled by different spanwer classes
#
# @return [ActiveRecord::Relation] Relation or collection proxy or some AR classs
def spawn
CLASS_TO_SPAWNER_CLASS_MAPPINGS[value.class].
spawn(*scope_arguments)
end
private
# :nodoc:
def scope_arguments
[scope, column_or_table_name, value, prefix]
end
# :nodoc:
class BasicScopeSpawner < ScopeSpawner
# :nodoc:
def spawn
scope.where(column_name => value)
end
private
# :nodoc:
def column_name
[prefix, column_or_table_name].compact.join(SEPERATOR)
end
end
# This class is only for inheritance
#
# @abstract
# Subclass has to implement #query_string & #processed_value
class EqualScopeSpawner < BasicScopeSpawner
# We generate query_string and pass values to it
# To avoid some scope chaining problems
def spawn
scope.where(query_string, processed_value)
end
end
# :nodoc:
class StringScopeSpawner < EqualScopeSpawner
# :nodoc:
def query_string
"lower(#{column_name}) = ?"
end
# :nodoc:
def processed_value
value.downcase
end
end
# :nodoc:
class RangeScopeSpawner < EqualScopeSpawner
# :nodoc:
def query_string
"lower(#{column_name}) IN (?)"
end
# :nodoc:
def processed_value
Range.new(value.begin.to_s.downcase, value.end.to_s.downcase, value.exclude_end?)
end
end
# :nodoc:
class ArrayScopeSpawner < EqualScopeSpawner
# :nodoc:
def query_string
"lower(#{column_name}) IN (?)"
end
# :nodoc:
def processed_value
value.to_a.map { |x| x.to_s.downcase }
end
end
# :nodoc:
class HashScopeSpawner < BasicScopeSpawner
# If prefix already exists,
# that means we are in association table already,
# which cannot accept another hash
# This gem has no ability to handle deep nested associaiton reflection yet
def spawn
fail TooDeepNestedConditions unless prefix.nil?
value.inject(scope) do |new_scope, (column_name, column_value)|
ScopeSpawner.spawn(new_scope, column_name, column_value, column_or_table_name)
end
end
end
# This was extracted from `case..when`
CLASS_TO_SPAWNER_CLASS_MAPPINGS = {
Hash => HashScopeSpawner,
String => StringScopeSpawner,
Range => RangeScopeSpawner,
Array => ArrayScopeSpawner,
}.tap { |h| h.default = BasicScopeSpawner }.freeze
end
end