WIP: refactor type casting behavior to better support ranasckers

This commit is contained in:
Ernie Miller 2011-04-10 21:11:28 -04:00
parent ecd42f4a83
commit 11dd0d63f4
8 changed files with 114 additions and 132 deletions

View File

@ -18,7 +18,8 @@ module Ransack
end
opts[:type] ||= :string
opts[:call] ||= block || lambda {|parent| parent.table[name]}
opts[:args] ||= [:parent]
opts[:callable] ||= block || (method(name) if method_defined?(name)) || proc {|parent| parent.table[name]}
_ransackers[name.to_s] = opts
end

View File

@ -61,7 +61,6 @@ module Ransack
@engine.connection_pool.columns_hash[table][name].type
end
private
def get_parent_and_attribute_name(str, parent = @base)

View File

@ -22,10 +22,6 @@ module Ransack
attr
end
def ransacker
klass._ransackers[attr_name] if klass.respond_to?(:_ransackers)
end
def type
if ransacker
return ransacker[:type]
@ -34,6 +30,14 @@ module Ransack
end
end
def apply_predicate_with_values(predicate, values)
attr.send(predicate.arel_predicate, predicate.format(values))
end
def cast_value(value)
value.cast_to_type(type)
end
def eql?(other)
self.class == other.class &&
self.name == other.name
@ -47,6 +51,7 @@ module Ransack
def persisted?
false
end
end
end
end

View File

@ -5,7 +5,11 @@ module Ransack
attr_accessor :parent, :attr_name
def attr
@attr ||= context.table_for(parent)[attr_name]
@attr ||= ransacker ? ransacker[:callable].call(parent) : context.table_for(parent)[attr_name]
end
def ransacker
klass._ransackers[attr_name] if klass.respond_to?(:_ransackers)
end
def klass

View File

@ -4,6 +4,8 @@ module Ransack
i18n_word :attribute, :predicate, :combinator, :value
i18n_alias :a => :attribute, :p => :predicate, :m => :combinator, :v => :value
delegate :cast_value, :to => :first_attribute
attr_reader :predicate
class << self
@ -42,6 +44,10 @@ module Ransack
values.size <= 1 || predicate.compound || %w(in not_in).include?(predicate.name)
end
def first_attribute
attributes.first
end
def attributes
@attributes ||= []
end
@ -74,12 +80,12 @@ module Ransack
case args
when Array
args.each do |val|
val = Value.new(@context, val, current_type)
val = Value.new(@context, val)
self.values << val
end
when Hash
args.each do |index, attrs|
val = Value.new(@context, attrs[:value], current_type)
val = Value.new(@context, attrs[:value])
self.values << val
end
else
@ -105,13 +111,13 @@ module Ransack
end
def build_value(val = nil)
Value.new(@context, val, current_type).tap do |value|
Value.new(@context, val).tap do |value|
self.values << value
end
end
def value
predicate.compound ? values.map(&:value) : values.first.value
predicate.compound ? values.map {|v| cast_value(v)} : cast_value(values.first)
end
def build(params)
@ -121,8 +127,6 @@ module Ransack
end
end
set_value_types!
self
end
@ -163,47 +167,29 @@ module Ransack
alias :p :predicate_name
def apply_predicate
attributes = arel_attributes.compact
if attributes.size > 1
case combinator
when 'and'
Arel::Nodes::Grouping.new(Arel::Nodes::And.new(
attributes.map {|a| a.send(predicate.arel_predicate, predicate.format(values))}
attributes.map {|a| a.apply_predicate_with_values(predicate, values)}
))
when 'or'
attributes.inject(attributes.shift.send(predicate.arel_predicate, predicate.format(values))) do |memo, a|
memo.or(a.send(predicate.arel_predicate, predicate.format(values)))
attributes.inject(attributes.shift.apply_predicate_with_values(predicate, values)) do |memo, a|
memo.or(a.apply_predicate_with_values(predicate, values))
end
end
else
attributes.first.send(predicate.arel_predicate, predicate.format(values))
attributes.first.apply_predicate_with_values(predicate, values)
end
end
private
def set_value_types!
self.values.each {|v| v.type = current_type}
end
def current_type
if predicate && predicate.type
predicate.type
elsif attributes.size > 0
attributes.first.type
end
end
def valid_combinator?
attributes.size < 2 ||
['and', 'or'].include?(combinator)
end
def arel_attributes
attributes.map(&:attr)
end
end
end
end

View File

@ -1,22 +1,12 @@
module Ransack
module Nodes
class Value < Node
attr_reader :value_before_cast, :type
delegate :blank?, :to => :value_before_cast
attr_accessor :value
delegate :blank?, :to => :value
def initialize(context, value = nil, type = nil)
def initialize(context, value = nil)
super(context)
@value_before_cast = value
self.type = type if type
end
def value=(val)
@value_before_cast = value
@value = nil
end
def value
@value ||= cast_to_type(@value_before_cast, @type)
@value = value
end
def persisted?
@ -25,96 +15,91 @@ module Ransack
def eql?(other)
self.class == other.class &&
self.value_before_cast == other.value_before_cast
self.value == other.value
end
alias :== :eql?
def hash
value_before_cast.hash
value.hash
end
def type=(type)
@value = nil
@type = type
end
def cast_to_type(type)
case type
when :date
cast_to_date(value)
when :datetime, :timestamp, :time
cast_to_time(value)
when :boolean
cast_to_boolean(value)
when :integer
cast_to_integer(value)
when :float
cast_to_float(value)
when :decimal
cast_to_decimal(value)
else
cast_to_string(value)
end
end
def cast_to_type(val, type)
case type
when :date
cast_to_date(val)
when :datetime, :timestamp, :time
cast_to_time(val)
when :boolean
cast_to_boolean(val)
when :integer
cast_to_integer(val)
when :float
cast_to_float(val)
when :decimal
cast_to_decimal(val)
else
cast_to_string(val)
end
end
def cast_to_date(val)
if val.respond_to?(:to_date)
val.to_date rescue nil
else
y, m, d = *[val].flatten
m ||= 1
d ||= 1
Date.new(y,m,d) rescue nil
end
end
def cast_to_date(val)
if val.respond_to?(:to_date)
val.to_date rescue nil
else
y, m, d = *[val].flatten
m ||= 1
d ||= 1
Date.new(y,m,d) rescue nil
end
end
# FIXME: doesn't seem to be casting, even with Time.zone.local
def cast_to_time(val)
if val.is_a?(Array)
Time.zone.local(*val) rescue nil
else
unless val.acts_like?(:time)
val = val.is_a?(String) ? Time.zone.parse(val) : val.to_time rescue val
end
val.in_time_zone
end
end
# FIXME: doesn't seem to be casting, even with Time.zone.local
def cast_to_time(val)
if val.is_a?(Array)
Time.zone.local(*val) rescue nil
else
unless val.acts_like?(:time)
val = val.is_a?(String) ? Time.zone.parse(val) : val.to_time rescue val
end
val.in_time_zone
end
end
def cast_to_boolean(val)
if val.is_a?(String) && val.blank?
nil
else
Constants::TRUE_VALUES.include?(val)
end
end
def cast_to_boolean(val)
if val.is_a?(String) && val.blank?
nil
else
Constants::TRUE_VALUES.include?(val)
end
end
def cast_to_string(val)
val.respond_to?(:to_s) ? val.to_s : String.new(val)
end
def cast_to_string(val)
val.respond_to?(:to_s) ? val.to_s : String.new(val)
end
def cast_to_integer(val)
val.blank? ? nil : val.to_i
end
def cast_to_integer(val)
val.blank? ? nil : val.to_i
end
def cast_to_float(val)
val.blank? ? nil : val.to_f
end
def cast_to_float(val)
val.blank? ? nil : val.to_f
end
def cast_to_decimal(val)
if val.blank?
nil
elsif val.class == BigDecimal
val
elsif val.respond_to?(:to_d)
val.to_d
else
val.to_s.to_d
end
end
def cast_to_decimal(val)
if val.blank?
nil
elsif val.class == BigDecimal
val
elsif val.respond_to?(:to_d)
val.to_d
else
val.to_s.to_d
end
end
def array_of_arrays?(val)
Array === val && Array === val.first
end
def array_of_arrays?(val)
Array === val && Array === val.first
end
end
end
end

View File

@ -27,11 +27,11 @@ module Ransack
def format(vals)
if formatter
vals.select {|v| validator ? validator.call(v.value_before_cast) : !v.blank?}.
map {|v| formatter.call(v.value)}
vals.select {|v| validator ? validator.call(v.value) : !v.blank?}.
map {|v| formatter.call(v.cast_to_type(type))}
else
vals.select {|v| validator ? validator.call(v.value_before_cast) : !v.blank?}.
map {|v| v.value}
vals.select {|v| validator ? validator.call(v.value) : !v.blank?}.
map {|v| v.cast_to_type(type)}
end
end
@ -47,7 +47,7 @@ module Ransack
def validate(vals)
if validator
vals.select {|v| validator.call(v.value_before_cast)}.any?
vals.select {|v| validator.call(v.value)}.any?
else
vals.select {|v| !v.blank?}.any?
end

View File

@ -26,9 +26,11 @@ module Ransack
describe '#ransacker' do
it 'creates ransack attributes' do
ancestors = Person.singleton_class.ancestors.size
Person.ransacker :backwards_name
Person.ransacker :backwards_name do |parent|
parent.table[:backwards_name]
end
s = Person.search(:backwards_name_eq => 'blah')
s.result.to_sql.should match /"people"."backwards_name" = 'blah'/
end
end