refactored AR parsing methods into Parser module to reduce AR method pollution and make more consistent

This commit is contained in:
Adam Meehan
2009-03-28 17:25:48 +11:00
parent 88fce1d679
commit 312c1510cb
10 changed files with 133 additions and 58 deletions

View File

@@ -1,4 +1,5 @@
require 'validates_timeliness/formats' require 'validates_timeliness/formats'
require 'validates_timeliness/parser'
require 'validates_timeliness/validator' require 'validates_timeliness/validator'
require 'validates_timeliness/validation_methods' require 'validates_timeliness/validation_methods'
require 'validates_timeliness/spec/rails/matchers/validate_timeliness' if ENV['RAILS_ENV'] == 'test' require 'validates_timeliness/spec/rails/matchers/validate_timeliness' if ENV['RAILS_ENV'] == 'test'
@@ -14,9 +15,11 @@ require 'validates_timeliness/core_ext/date_time'
module ValidatesTimeliness module ValidatesTimeliness
mattr_accessor :default_timezone mattr_accessor :default_timezone
self.default_timezone = :utc self.default_timezone = :utc
mattr_accessor :use_time_zones
self.use_time_zones = false
LOCALE_PATH = File.expand_path(File.dirname(__FILE__) + '/validates_timeliness/locale/en.yml') LOCALE_PATH = File.expand_path(File.dirname(__FILE__) + '/validates_timeliness/locale/en.yml')
class << self class << self
@@ -46,10 +49,10 @@ module ValidatesTimeliness
end end
def setup_for_rails def setup_for_rails
major, minor = Rails::VERSION::MAJOR, Rails::VERSION::MINOR
self.default_timezone = ::ActiveRecord::Base.default_timezone self.default_timezone = ::ActiveRecord::Base.default_timezone
self.enable_datetime_select_extension! self.use_time_zones = ::ActiveRecord::Base.time_zone_aware_attributes rescue false
self.load_error_messages enable_datetime_select_extension!
load_error_messages
end end
end end
end end

View File

@@ -46,7 +46,7 @@ module ValidatesTimeliness
# implementation as it chains the write_attribute method which deletes # implementation as it chains the write_attribute method which deletes
# the attribute from the cache. # the attribute from the cache.
def write_date_time_attribute(attr_name, value, type, time_zone_aware) def write_date_time_attribute(attr_name, value, type, time_zone_aware)
new = self.class.parse_date_time(value, type) new = ValidatesTimeliness::Parser.parse(value, type)
if new && type != :date if new && type != :date
new = new.to_time new = new.to_time
@@ -73,7 +73,7 @@ module ValidatesTimeliness
if @attributes_cache.has_key?(attr_name) if @attributes_cache.has_key?(attr_name)
time = read_attribute_before_type_cast(attr_name) time = read_attribute_before_type_cast(attr_name)
time = self.class.parse_date_time(time, type) time = ValidatesTimeliness::Parser.parse(time, type)
else else
time = read_attribute(attr_name) time = read_attribute(attr_name)
@attributes[attr_name] = time && time_zone_aware ? time.in_time_zone : time @attributes[attr_name] = time && time_zone_aware ? time.in_time_zone : time
@@ -83,8 +83,6 @@ module ValidatesTimeliness
module ClassMethods module ClassMethods
# Define attribute reader and writer method for date, time and
# datetime attributes to use plugin parser.
def define_attribute_methods_with_timeliness def define_attribute_methods_with_timeliness
return if generated_methods? return if generated_methods?
columns_hash.each do |name, column| columns_hash.each do |name, column|
@@ -105,7 +103,6 @@ module ValidatesTimeliness
define_attribute_methods_without_timeliness define_attribute_methods_without_timeliness
end end
# Define write method for date, time and datetime columns
def define_write_method_for_dates_and_times(attr_name, type, time_zone_aware) def define_write_method_for_dates_and_times(attr_name, type, time_zone_aware)
method_body = <<-EOV method_body = <<-EOV
def #{attr_name}=(value) def #{attr_name}=(value)

View File

@@ -38,7 +38,7 @@ module ValidatesTimeliness
end end
def time_array_to_string(values, type) def time_array_to_string(values, type)
values = values.map {|v| v.to_s } values.collect! {|v| v.to_s }
case type case type
when :date when :date

View File

@@ -0,0 +1,45 @@
module ValidatesTimeliness
module Parser
class << self
def parse(raw_value, type, strict=true)
return nil if raw_value.blank?
return raw_value if raw_value.acts_like?(:time) || raw_value.is_a?(Date)
time_array = ValidatesTimeliness::Formats.parse(raw_value, type, strict)
raise if time_array.nil?
# Rails dummy time date part is defined as 2000-01-01
time_array[0..2] = 2000, 1, 1 if type == :time
# Date.new enforces days per month, unlike Time
date = Date.new(*time_array[0..2]) unless type == :time
return date if type == :date
# Create time object which checks time part, and return time object
make_time(time_array)
rescue
nil
end
def make_time(time_array)
if Time.respond_to?(:zone) && ValidatesTimeliness.use_time_zones
Time.zone.local(*time_array)
else
begin
time_zone = ValidatesTimeliness.default_timezone
Time.send(time_zone, *time_array)
rescue ArgumentError, TypeError
zone_offset = time_zone == :local ? DateTime.local_offset : 0
time_array.pop # remove microseconds
DateTime.civil(*(time_array << zone_offset))
end
end
end
end
end
end

View File

@@ -7,27 +7,6 @@ module ValidatesTimeliness
module ClassMethods module ClassMethods
def parse_date_time(raw_value, type, strict=true)
return nil if raw_value.blank?
return raw_value if raw_value.acts_like?(:time) || raw_value.is_a?(Date)
time_array = ValidatesTimeliness::Formats.parse(raw_value, type, strict)
raise if time_array.nil?
# Rails dummy time date part is defined as 2000-01-01
time_array[0..2] = 2000, 1, 1 if type == :time
# Date.new enforces days per month, unlike Time
date = Date.new(*time_array[0..2]) unless type == :time
return date if type == :date
# Create time object which checks time part, and return time object
make_time(time_array)
rescue
nil
end
def validates_time(*attr_names) def validates_time(*attr_names)
configuration = attr_names.extract_options! configuration = attr_names.extract_options!
configuration[:type] = :time configuration[:type] = :time
@@ -59,21 +38,6 @@ module ValidatesTimeliness
end end
end end
# Time.zone. Rails 2.0 should be default_timezone.
def make_time(time_array)
if Time.respond_to?(:zone) && time_zone_aware_attributes
Time.zone.local(*time_array)
else
begin
Time.send(::ActiveRecord::Base.default_timezone, *time_array)
rescue ArgumentError, TypeError
zone_offset = ::ActiveRecord::Base.default_timezone == :local ? DateTime.local_offset : 0
time_array.pop # remove microseconds
DateTime.civil(*(time_array << zone_offset))
end
end
end
end end
end end

View File

@@ -36,7 +36,7 @@ module ValidatesTimeliness
end end
def call(record, attr_name, value) def call(record, attr_name, value)
value = record.class.parse_date_time(value, type, false) if value.is_a?(String) value = ValidatesTimeliness::Parser.parse(value, type, false) if value.is_a?(String)
raw_value = raw_value(record, attr_name) || value raw_value = raw_value(record, attr_name) || value
return if (raw_value.nil? && configuration[:allow_nil]) || (raw_value.blank? && configuration[:allow_blank]) return if (raw_value.nil? && configuration[:allow_nil]) || (raw_value.blank? && configuration[:allow_blank])
@@ -143,7 +143,7 @@ module ValidatesTimeliness
end end
date, time = self.class.evaluate_option_value(date, :date, record), self.class.evaluate_option_value(time, :time, record) date, time = self.class.evaluate_option_value(date, :date, record), self.class.evaluate_option_value(time, :time, record)
return if date.nil? || time.nil? return if date.nil? || time.nil?
record.class.send(:make_time, [date.year, date.month, date.day, time.hour, time.min, time.sec, time.usec]) ValidatesTimeliness::Parser.make_time([date.year, date.month, date.day, time.hour, time.min, time.sec, time.usec])
end end
def validate_options(options) def validate_options(options)
@@ -158,7 +158,7 @@ module ValidatesTimeliness
def evaluate_option_value(value, type, record) def evaluate_option_value(value, type, record)
case value case value
when Time, Date, DateTime when Time, Date
value value
when Symbol when Symbol
evaluate_option_value(record.send(value), type, record) evaluate_option_value(record.send(value), type, record)
@@ -169,7 +169,7 @@ module ValidatesTimeliness
when Range when Range
evaluate_option_value([value.first, value.last], type, record) evaluate_option_value([value.first, value.last], type, record)
else else
record.class.parse_date_time(value, type, false) ValidatesTimeliness::Parser.parse(value, type, false)
end end
end end
@@ -192,7 +192,7 @@ module ValidatesTimeliness
nil nil
end end
if ignore_usec && value.is_a?(Time) if ignore_usec && value.is_a?(Time)
::ActiveRecord::Base.send(:make_time, Array(value).reverse[4..9]) ValidatesTimeliness::Parser.make_time(Array(value).reverse[4..9])
else else
value value
end end

View File

@@ -39,17 +39,17 @@ describe ValidatesTimeliness::ActiveRecord::AttributeMethods do
end end
it "should call parser on write for datetime attribute" do it "should call parser on write for datetime attribute" do
@person.class.should_receive(:parse_date_time).once ValidatesTimeliness::Parser.should_receive(:parse).once
@person.birth_date_and_time = "2000-01-01 02:03:04" @person.birth_date_and_time = "2000-01-01 02:03:04"
end end
it "should call parser on write for date attribute" do it "should call parser on write for date attribute" do
@person.class.should_receive(:parse_date_time).once ValidatesTimeliness::Parser.should_receive(:parse).once
@person.birth_date = "2000-01-01" @person.birth_date = "2000-01-01"
end end
it "should call parser on write for time attribute" do it "should call parser on write for time attribute" do
@person.class.should_receive(:parse_date_time).once ValidatesTimeliness::Parser.should_receive(:parse).once
@person.birth_time = "12:00" @person.birth_time = "12:00"
end end

61
spec/parser_spec.rb Normal file
View File

@@ -0,0 +1,61 @@
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe ValidatesTimeliness::Parser do
attr_accessor :person
describe "parse" do
it "should return time object for valid time string" do
parse("2000-01-01 12:13:14", :datetime).should be_kind_of(Time)
end
it "should return nil for time string with invalid date part" do
parse("2000-02-30 12:13:14", :datetime).should be_nil
end
it "should return nil for time string with invalid time part" do
parse("2000-02-01 25:13:14", :datetime).should be_nil
end
it "should return Time object when passed a Time object" do
parse(Time.now, :datetime).should be_kind_of(Time)
end
if RAILS_VER >= '2.1'
it "should convert time string into current timezone" do
Time.zone = 'Melbourne'
time = parse("2000-01-01 12:13:14", :datetime)
Time.zone.utc_offset.should == 10.hours
end
end
it "should return nil for invalid date string" do
parse("2000-02-30", :date).should be_nil
end
def parse(*args)
ValidatesTimeliness::Parser.parse(*args)
end
end
describe "make_time" do
if RAILS_VER >= '2.1'
it "should create time using current timezone" do
Time.zone = 'Melbourne'
time = ValidatesTimeliness::Parser.make_time([2000,1,1,12,0,0])
time.zone.should == "EST"
end
else
it "should create time using default timezone" do
time = ValidatesTimeliness::Parser.make_time([2000,1,1,12,0,0])
time.zone.should == "UTC"
end
end
end
end

View File

@@ -30,6 +30,7 @@ require 'active_record'
require 'active_record/version' require 'active_record/version'
require 'action_controller' require 'action_controller'
require 'action_view' require 'action_view'
require 'action_mailer'
require 'spec/rails' require 'spec/rails'
require 'time_travel/time_travel' require 'time_travel/time_travel'
@@ -38,13 +39,13 @@ ActiveRecord::Base.default_timezone = :utc
RAILS_VER = Rails::VERSION::STRING RAILS_VER = Rails::VERSION::STRING
puts "Using #{vendored ? 'vendored' : 'gem'} Rails version #{RAILS_VER} (ActiveRecord version #{ActiveRecord::VERSION::STRING})" puts "Using #{vendored ? 'vendored' : 'gem'} Rails version #{RAILS_VER} (ActiveRecord version #{ActiveRecord::VERSION::STRING})"
require 'validates_timeliness'
if RAILS_VER >= '2.1' if RAILS_VER >= '2.1'
Time.zone_default = ActiveSupport::TimeZone['UTC'] Time.zone_default = ActiveSupport::TimeZone['UTC']
ActiveRecord::Base.time_zone_aware_attributes = true ActiveRecord::Base.time_zone_aware_attributes = true
end end
require 'validates_timeliness'
ActiveRecord::Migration.verbose = false ActiveRecord::Migration.verbose = false
ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'}) ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'})

View File

@@ -66,7 +66,7 @@ describe ValidatesTimeliness::Validator do
it "should return array of Time objects when restriction is array of strings" do it "should return array of Time objects when restriction is array of strings" do
time1, time2 = "2000-01-02", "2000-01-01" time1, time2 = "2000-01-02", "2000-01-01"
evaluate_option_value([time1, time2], :datetime).should == [Person.parse_date_time(time2, :datetime), Person.parse_date_time(time1, :datetime)] evaluate_option_value([time1, time2], :datetime).should == [parse(time2, :datetime), parse(time1, :datetime)]
end end
it "should return array of Time objects when restriction is Range of Time objects" do it "should return array of Time objects when restriction is Range of Time objects" do
@@ -76,7 +76,7 @@ describe ValidatesTimeliness::Validator do
it "should return array of Time objects when restriction is Range of time strings" do it "should return array of Time objects when restriction is Range of time strings" do
time1, time2 = "2000-01-02", "2000-01-01" time1, time2 = "2000-01-02", "2000-01-01"
evaluate_option_value(time1..time2, :datetime).should == [Person.parse_date_time(time2, :datetime), Person.parse_date_time(time1, :datetime)] evaluate_option_value(time1..time2, :datetime).should == [parse(time2, :datetime), parse(time1, :datetime)]
end end
def evaluate_option_value(restriction, type) def evaluate_option_value(restriction, type)
configure_validator(:type => type) configure_validator(:type => type)
@@ -587,6 +587,10 @@ describe ValidatesTimeliness::Validator do
end end
def parse(*args)
ValidatesTimeliness::Parser.parse(*args)
end
def configure_validator(options={}) def configure_validator(options={})
@validator = ValidatesTimeliness::Validator.new(options) @validator = ValidatesTimeliness::Validator.new(options)
end end