commit 591ea3126beff04301984d537169ee8395e43591 Author: Adam Meehan Date: Fri May 2 16:42:51 2008 +1000 first commit diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..80cdbc2 --- /dev/null +++ b/Rakefile @@ -0,0 +1,12 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' +require 'spec/rake/spectask' + +spec_files = Rake::FileList["spec/**/*_spec.rb"] + +desc "Run specs" +Spec::Rake::SpecTask.new do |t| + t.spec_files = spec_files + t.spec_opts = ["-c"] +end diff --git a/init.rb b/init.rb new file mode 100644 index 0000000..8bee271 --- /dev/null +++ b/init.rb @@ -0,0 +1,3 @@ +raise "Rails version must be 2.0 or greater to use validates_timeliness plugin" if Rails::VERSION::MAJOR < 2 + +require 'validates_timeliness' diff --git a/lib/validates_timeliness.rb b/lib/validates_timeliness.rb new file mode 100644 index 0000000..c828446 --- /dev/null +++ b/lib/validates_timeliness.rb @@ -0,0 +1,7 @@ +require 'validates_timeliness/base' +require 'validates_timeliness/attribute_methods' +require 'validates_timeliness/validations' + +ActiveRecord::Base.send(:include, ValidatesTimeliness::Base) +ActiveRecord::Base.send(:include, ValidatesTimeliness::AttributeMethods) +ActiveRecord::Base.send(:include, ValidatesTimeliness::Validations) diff --git a/lib/validates_timeliness/attribute_methods.rb b/lib/validates_timeliness/attribute_methods.rb new file mode 100644 index 0000000..99bd3aa --- /dev/null +++ b/lib/validates_timeliness/attribute_methods.rb @@ -0,0 +1,38 @@ +module ValidatesTimeliness + module AttributeMethods + + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + def define_read_method_for_time_zone_conversion(attr_name) + method_body = <<-EOV + def #{attr_name}(reload = false) + cached = @attributes_cache['#{attr_name}'] + return cached if cached && !reload + time = read_attribute('#{attr_name}') + unless time.acts_like?(:time) + time = time.to_time(:local) rescue nil + end + @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time + end + EOV + evaluate_attribute_method attr_name, method_body + end + + def define_write_method_for_time_zone_conversion(attr_name) + method_body = <<-EOV + def #{attr_name}=(time) + if time + time = time.in_time_zone if time.acts_like?(:time) + end + write_attribute(:#{attr_name}, time) + end + EOV + evaluate_attribute_method attr_name, method_body, "#{attr_name}=" + end + end + + end +end diff --git a/lib/validates_timeliness/base.rb b/lib/validates_timeliness/base.rb new file mode 100644 index 0000000..bef4e44 --- /dev/null +++ b/lib/validates_timeliness/base.rb @@ -0,0 +1,34 @@ +module ValidatesTimeliness + module Base + + def time_array_to_string(time_array) + time_array.collect! {|i| i.to_s.rjust(2, '0') } + time_array[0..2].join('-') + ' ' + time_array[3..5].join(':') + end + + def execute_callstack_for_multiparameter_attributes(callstack) + errors = [] + callstack.each do |name, values| + klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass + if values.empty? + send(name + "=", nil) + else + begin + value = if Time == klass || Date == klass + time_array_to_string(values) + else + klass.new(*values) + end + send(name + "=", value) + rescue => ex + errors << AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name) + end + end + end + unless errors.empty? + raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes" + end + end + + end +end diff --git a/lib/validates_timeliness/validations.rb b/lib/validates_timeliness/validations.rb new file mode 100644 index 0000000..f324359 --- /dev/null +++ b/lib/validates_timeliness/validations.rb @@ -0,0 +1,48 @@ +module ValidatesTimeliness + module Validations + + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + + def validates_timeliness_of(*attr_names) + # possible options only_date only_time only_epoch + configuration = { :on => :save, :allow_nil => false } + configuration.update(attr_names.extract_options!) + + restriction_methods = {:before => '<', :after => '>', :on_or_before => '<=', :on_or_after => '>='} + + validates_each(attr_names, configuration) do |record, attr_name, value| + raw_value = record.send("#{attr_name}_before_type_cast") || value + + next if raw_value.is_nil? and options[:allow_nil] + + begin + time_array = ParseDate.parsedate(raw_value) + + # checks if date is valid and enforces number of days in a month unlike Time + date = Date.new(*time_array[0..2]) + + # checks if time is valid as it will accept bad date values + time = Time.mktime(*time_array) + + restriction_methods.each do |option, method| + if restriction = options[option] + restriction = restriction.to_time + record.errors.add(attr_name, "must be #{humanize(option)} #{restriction}") unless time.send(method, restriction) + end + end + rescue + record.errors.add(attr_name, "is not a valid time") + next + end + + end + end + + end + + end +end diff --git a/spec/attribute_methods_spec.rb b/spec/attribute_methods_spec.rb new file mode 100644 index 0000000..1be5d79 --- /dev/null +++ b/spec/attribute_methods_spec.rb @@ -0,0 +1,75 @@ +require File.dirname(__FILE__) + '/spec_helper' + +describe ValidatesTimeliness::AttributeMethods do + + describe "for Time columns" do + before do + Person.define_read_method_for_time_zone_conversion(:birth_date_and_time) + Person.define_write_method_for_time_zone_conversion(:birth_date_and_time) + @person = Person.new + end + + it "should define attribute read method for column" do + @person.respond_to?(:birth_date_and_time).should be_true + end + + it "should define attribute write method for column" do + @person.respond_to?(:birth_date_and_time=).should be_true + end + + it "should return string value for attribute_before_type_cast when written as string" do + @person.birth_date_and_time = "1980-12-25 01:02:03" + @person.birth_date_and_time_before_type_cast.should == "1980-12-25 01:02:03" + end + + it "should return Time object for attribute_before_type_cast when written as Time" do + @person.birth_date_and_time = time = Time.mktime(1980, 12, 25, 1, 2, 3) + @person.birth_date_and_time_before_type_cast.should == time + end + + it "should return Time object using attribute read method when written with string" do + @person.birth_date_and_time = "1980-12-25 01:02:03" + @person.birth_date_and_time.should == Time.mktime(1980, 12, 25, 1, 2, 3) + end + + it "should read stored time with correct timezone" + + it "should return nil when date is invalid" + end + + describe "for Date columns" do + before do + Person.define_read_method_for_time_zone_conversion(:birth_date) + Person.define_write_method_for_time_zone_conversion(:birth_date) + @person = Person.new + end + + it "should define attribute read method for column" do + @person.respond_to?(:birth_date).should be_true + end + + it "should define attribute write method for column" do + @person.respond_to?(:birth_date=).should be_true + end + + it "should return string value for attribute_before_type_cast when written as string" do + @person.birth_date = "1980-12-25" + @person.birth_date_before_type_cast.should == "1980-12-25" + end + + it "should return Date object for attribute_before_type_cast when written as Date" do + @person.birth_date = date = Date.new(1980, 12, 25) + @person.birth_date_before_type_cast.should == date + end + + it "should return Date object using attribute read method when written with string" do + @person.birth_date = "1980-12-25" + @person.birth_date.should == Date.new(1980, 12, 25) + end + + it "should read stored time with correct timezone" + + it "should return nil when date is invalid" + end + +end diff --git a/spec/base_spec.rb b/spec/base_spec.rb new file mode 100644 index 0000000..eae5f16 --- /dev/null +++ b/spec/base_spec.rb @@ -0,0 +1,37 @@ +require File.dirname(__FILE__) + '/spec_helper' + +describe ValidatesTimeliness::Base do + + class AttributeAssignmentError; def initialize(*args); end; end + class MultiparameterAssignmentErrors; def initialize(*args); end; end + + before do + self.class.stub!(:reflect_on_aggregation).and_return(nil) + + end + + it "should convert time array into string" do + time_string = time_array_to_string([2000,1,1,12,12,0]) + time_string.should == "2000-01-01 12:12:00" + end + + describe "execute_callstack_for_multiparameter_attributes" do + before do + @date_array = [1980,1,1,0,0,0] + end + + it "should store time string for a Time class column" do + self.stub!(:column_for_attribute).and_return( mock('Column', :klass => Time) ) + self.should_receive(:birth_date_and_time=).once.with("1980-01-01 00:00:00") + callstack = {'birth_date_and_time' => @date_array} + execute_callstack_for_multiparameter_attributes(callstack) + end + + it "should store time string for a Date class column" do + self.stub!(:column_for_attribute).and_return( mock('Column', :klass => Date) ) + self.should_receive(:birth_date=).once.with("1980-01-01 00:00:00") + callstack = {'birth_date' => @date_array} + execute_callstack_for_multiparameter_attributes(callstack) + end + end +end diff --git a/spec/resources/person.rb b/spec/resources/person.rb new file mode 100644 index 0000000..c2e188d --- /dev/null +++ b/spec/resources/person.rb @@ -0,0 +1,3 @@ +class Person < ActiveRecord::Base + +end diff --git a/spec/resources/schema.rb b/spec/resources/schema.rb new file mode 100644 index 0000000..a6bb723 --- /dev/null +++ b/spec/resources/schema.rb @@ -0,0 +1,9 @@ +ActiveRecord::Schema.define(:version => 1) do + + create_table "people", :force => true do |t| + t.string "name" + t.datetime "birth_date_and_time" + t.date "birth_date" + end + +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..336e427 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,18 @@ +$: << File.dirname(__FILE__) + '/../lib' << File.dirname(__FILE__) + +require 'rubygems' +require 'spec' +require 'active_support' +require 'active_record' + +require 'validates_timeliness' + +conn = { + :adapter => 'sqlite3', + :database => ':memory:' +} + +ActiveRecord::Base.establish_connection(conn) + +require 'resources/schema' +require 'resources/person'