diff --git a/README.rdoc b/README.rdoc index d199627..cb3269a 100644 --- a/README.rdoc +++ b/README.rdoc @@ -42,13 +42,18 @@ This creates configuration initializer and locale files. In the initializer, you ValidatesTimeliness.setup do |config| - # Add validation helpers to these classes - # config.extend_classes = [ ActiveRecord::Base ] + # Add plugin to supported ORMs (only :active_record for now) + # config.extend_orms = [ :active_record ] end -By default the plugin extends ActiveRecord if present. If you are using one or more other ORMs, you need to add them to this config option array. -As long as the ORM supports ActiveModel validations, it should work. +By default the plugin extends ActiveRecord if present. Currently ActiveRecord is the only ORM included for extension. If you wish to extend +another ORM then look at the shim for ActiveRecord to see how to setup the hooks. +http://github.com/adzap/validates_timeliness/tree/master/lib/validates_timeliness/orms/active_record.rb + +To extend other ORMs is pretty straight forward. It matter of hooking into a couple of methods, being the attribute method generation and +timezone handling of validated attributes. However, the plugin must support the ActiveModel validations system. If you extend an ORM +successfully, please send me a pull request to add the shim to the plugin or let me know where to find it. == Usage: diff --git a/lib/generators/validates_timeliness/templates/validates_timeliness.rb b/lib/generators/validates_timeliness/templates/validates_timeliness.rb index dd5e760..ca534e0 100644 --- a/lib/generators/validates_timeliness/templates/validates_timeliness.rb +++ b/lib/generators/validates_timeliness/templates/validates_timeliness.rb @@ -1,6 +1,6 @@ ValidatesTimeliness.setup do |config| - # Add validation helpers to these classes - # config.extend_classes = [ ActiveRecord::Base ] + # Add plugin to supported ORMs (only :active_record for now) + # config.extend_orms = [ :active_record ] # # Set the dummy date part for a time type values. # config.dummy_date_for_time_type = [ 2000, 1, 1 ] diff --git a/lib/validates_timeliness.rb b/lib/validates_timeliness.rb index 58e2785..ee12560 100644 --- a/lib/validates_timeliness.rb +++ b/lib/validates_timeliness.rb @@ -15,9 +15,9 @@ require 'active_support/core_ext/date_time/zones' module ValidatesTimeliness autoload :VERSION, 'validates_timeliness/version' - # Add validation helpers to these classes - mattr_accessor :extend_classes - @@extend_classes = [ defined?(ActiveRecord) && ActiveRecord::Base ].compact + # Add plugin to supported ORMs (only :active_record for now) + mattr_accessor :extend_orms + @@extend_orms = [ defined?(ActiveRecord) && :active_record ].compact # Set the dummy date part for a time type values. mattr_accessor :dummy_date_for_time_type @@ -37,10 +37,7 @@ module ValidatesTimeliness # Setup method for plugin configuration def self.setup yield self - extend_classes.each {|klass| - klass.send(:include, ValidatesTimeliness::HelperMethods) - klass.send(:include, ValidatesTimeliness::AttributeMethods) - } + extend_orms.each {|orm| require "validates_timeliness/orms/#{orm}" } end end diff --git a/lib/validates_timeliness/attribute_methods.rb b/lib/validates_timeliness/attribute_methods.rb index ba55343..53a6c0a 100644 --- a/lib/validates_timeliness/attribute_methods.rb +++ b/lib/validates_timeliness/attribute_methods.rb @@ -2,28 +2,39 @@ module ValidatesTimeliness module AttributeMethods extend ActiveSupport::Concern - included do - if attribute_method_matchers.any? {|m| m.suffix == "_before_type_cast" && m.prefix.blank? } - extend BeforeTypeCastMethods - end - end - module ClassMethods + def define_timeliness_methods(before_type_cast=false) + timeliness_validated_attributes.each do |attr_name, type| + define_timeliness_write_method(attr_name, type, timeliness_attribute_timezone_aware?(attr_name)) + define_timeliness_before_type_cast_method(attr_name) if before_type_cast + end + end + protected - def define_method_attribute=(attr_name) - if timeliness_validated_attributes.include?(attr_name) - method_body, line = <<-EOV, __LINE__ + 1 - def #{attr_name}=(value) - @attributes_cache ||= {} - @attributes_cache["_#{attr_name}_before_type_cast"] = value - super - end - EOV - class_eval(method_body, __FILE__, line) - end - super rescue(NoMethodError) + def define_timeliness_write_method(attr_name, type, timezone_aware) + method_body, line = <<-EOV, __LINE__ + 1 + def #{attr_name}=(value) + @attributes_cache ||= {} + @attributes_cache["_#{attr_name}_before_type_cast"] = value + super + end + EOV + class_eval(method_body, __FILE__, line) + end + + def define_timeliness_before_type_cast_method(attr_name) + method_body, line = <<-EOV, __LINE__ + 1 + def #{attr_name}_before_type_cast + _timeliness_raw_value_for('#{attr_name}') + end + EOV + class_eval(method_body, __FILE__, line) + end + + def timeliness_attribute_timezone_aware?(attr_name) + false end end @@ -36,22 +47,5 @@ module ValidatesTimeliness end - module BeforeTypeCastMethods - - def define_method_attribute_before_type_cast(attr_name) - if timeliness_validated_attributes.include?(attr_name) - method_body, line = <<-EOV, __LINE__ + 1 - def #{attr_name}_before_type_cast - _timeliness_raw_value_for('#{attr_name}') - end - EOV - class_eval(method_body, __FILE__, line) - else - super rescue(NoMethodError) - end - end - - end - end end diff --git a/lib/validates_timeliness/helper_methods.rb b/lib/validates_timeliness/helper_methods.rb index bf3af71..4341052 100644 --- a/lib/validates_timeliness/helper_methods.rb +++ b/lib/validates_timeliness/helper_methods.rb @@ -5,30 +5,37 @@ module ValidatesTimeliness included do include ValidationMethods extend ValidationMethods + class_inheritable_accessor :timeliness_validated_attributes + self.timeliness_validated_attributes = {} end module ValidationMethods + def validates_timeliness_of(*attr_names) + options = _merge_attributes(attr_names) + attributes = options[:attributes].inject({}) {|validated, attr_name| + attr_name = attr_name.to_s + validated[attr_name] = options[:type] + validated + } + timeliness_validated_attributes.update(attributes) + validates_with Validator, options + end + def validates_date(*attr_names) - validates_with Validator, _merge_attributes(attr_names).merge(:type => :date) + options = attr_names.extract_options! + validates_timeliness_of *(attr_names << options.merge(:type => :date)) end def validates_time(*attr_names) - validates_with Validator, _merge_attributes(attr_names).merge(:type => :time) + options = attr_names.extract_options! + validates_timeliness_of *(attr_names << options.merge(:type => :time)) end def validates_datetime(*attr_names) - validates_with Validator, _merge_attributes(attr_names).merge(:type => :datetime) + options = attr_names.extract_options! + validates_timeliness_of *(attr_names << options.merge(:type => :datetime)) end - end - module ClassMethods - def timeliness_validated_attributes - @timeliness_validated_attributes ||= begin - _validators.map do |attr_name, validators| - attr_name.to_s if validators.any? {|v| v.is_a?(ValidatesTimeliness::Validator) } - end.compact - end - end end end end diff --git a/lib/validates_timeliness/orms/active_record.rb b/lib/validates_timeliness/orms/active_record.rb new file mode 100644 index 0000000..88bb7ee --- /dev/null +++ b/lib/validates_timeliness/orms/active_record.rb @@ -0,0 +1,14 @@ +class ActiveRecord::Base + include ValidatesTimeliness::HelperMethods + include ValidatesTimeliness::AttributeMethods + + def self.define_attribute_methods + super + # Define write method and before_type_cast method + define_timeliness_methods(true) + end + + def self.timeliness_attribute_timezone_aware?(attr_name) + create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) + end +end diff --git a/lib/validates_timeliness/validator.rb b/lib/validates_timeliness/validator.rb index 581258e..4136f36 100644 --- a/lib/validates_timeliness/validator.rb +++ b/lib/validates_timeliness/validator.rb @@ -18,9 +18,13 @@ module ValidatesTimeliness :timeliness end + def setup(klass) + @klass = klass + end + def initialize(options) - @allow_nil, @allow_blank = options.delete(:allow_nil), options.delete(:allow_blank) @type = options.delete(:type) || :datetime + @allow_nil, @allow_blank = options.delete(:allow_nil), options.delete(:allow_blank) @restrictions_to_check = RESTRICTIONS.keys & options.keys if range = options.delete(:between) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ce2aad4..1a681cb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -13,9 +13,10 @@ require 'rspec_tag_matchers' require 'model_helpers' require 'validates_timeliness' +require 'test_model' ValidatesTimeliness.setup do |c| - c.extend_classes = [ ActiveModel::Validations, ActiveRecord::Base ] + c.extend_orms = [ :active_record ] c.enable_date_time_select_extension! c.enable_multiparameter_extension! end @@ -25,22 +26,38 @@ Time.zone = 'Australia/Melbourne' LOCALE_PATH = File.expand_path(File.dirname(__FILE__) + '/../lib/generators/validates_timeliness/templates/en.yml') I18n.load_path.unshift(LOCALE_PATH) -class Person - include ActiveModel::AttributeMethods - include ActiveModel::Validations - extend ActiveModel::Translation +# Extend TestModel as you would another ORM/ODM module +module TestModel + include ValidatesTimeliness::HelperMethods + include ValidatesTimeliness::AttributeMethods - attr_accessor :birth_date, :birth_time, :birth_datetime - attr_accessor :attributes + def self.included(base) + base.extend HookMethods + end - def initialize(attributes = {}) - @attributes = {} - attributes.each do |key, value| - send "#{key}=", value + module HookMethods + # Hook method for attribute method generation + def define_attribute_methods(attr_names) + super + define_timeliness_methods + end + + # Hook into native time zone handling check, if any + def timeliness_attribute_timezone_aware?(attr_name) + false end end end +class Person + include TestModel + self.model_attributes = :birth_date, :birth_time, :birth_datetime + validates_date :birth_date + validates_time :birth_time + validates_datetime :birth_datetime + define_attribute_methods model_attributes +end + ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'}) ActiveRecord::Migration.verbose = false ActiveRecord::Schema.define(:version => 1) do @@ -54,6 +71,10 @@ ActiveRecord::Schema.define(:version => 1) do end class Employee < ActiveRecord::Base + validates_date :birth_date + validates_time :birth_time + validates_datetime :birth_datetime + define_attribute_methods end Rspec.configure do |c| @@ -61,8 +82,10 @@ Rspec.configure do |c| c.include(RspecTagMatchers) c.before do Person.reset_callbacks(:validate) + Person.timeliness_validated_attributes = {} Person._validators.clear Employee.reset_callbacks(:validate) + Employee.timeliness_validated_attributes = {} Employee._validators.clear end end diff --git a/spec/test_model.rb b/spec/test_model.rb new file mode 100644 index 0000000..c456d53 --- /dev/null +++ b/spec/test_model.rb @@ -0,0 +1,56 @@ +module TestModel + extend ActiveSupport::Concern + + included do + extend ActiveModel::Translation + include ActiveModel::Validations + include ActiveModel::AttributeMethods + include DynamicMethods + + attribute_method_suffix "" + attribute_method_suffix "=" + cattr_accessor :model_attributes + end + + module ClassMethods + def define_method_attribute=(attr_name) + generated_attribute_methods.module_eval("def #{attr_name}=(new_value); @attributes['#{attr_name}']=new_value ; end", __FILE__, __LINE__) + end + + def define_method_attribute(attr_name) + generated_attribute_methods.module_eval("def #{attr_name}; @attributes['#{attr_name}']; end", __FILE__, __LINE__) + end + end + + module DynamicMethods + def method_missing(method_id, *args, &block) + if !self.class.attribute_methods_generated? + self.class.define_attribute_methods self.class.model_attributes.map(&:to_s) + method_name = method_id.to_s + send(method_id, *args, &block) + else + super + end + end + end + + def initialize(attributes = nil) + @attributes = self.class.model_attributes.inject({}) do |hash, column| + hash[column.to_s] = nil + hash + end + self.attributes = attributes unless attributes.nil? + end + + def attributes + @attributes.keys + end + + def attributes=(new_attributes={}) + new_attributes.each do |key, value| + send "#{key}=", value + end + end + +end + diff --git a/spec/validates_timeliness/attribute_methods_spec.rb b/spec/validates_timeliness/attribute_methods_spec.rb index c778e25..6dc3632 100644 --- a/spec/validates_timeliness/attribute_methods_spec.rb +++ b/spec/validates_timeliness/attribute_methods_spec.rb @@ -1,20 +1,18 @@ require 'spec_helper' describe ValidatesTimeliness::AttributeMethods do - before do - Employee.validates_datetime :birth_datetime - Employee.define_attribute_methods - Person.validates_datetime :birth_datetime - Person.define_attribute_methods [:birth_datetime] - end - it 'should define _timeliness_raw_value_for instance method' do Person.instance_methods.should include('_timeliness_raw_value_for') end context "attribute write method" do + class EmployeeCopy < ActiveRecord::Base + set_table_name 'employees' + validates_datetime :birth_datetime + end + it 'should cache attribute raw value' do - r = Employee.new + r = EmployeeCopy.new r.birth_datetime = date_string = '2010-01-01' r._timeliness_raw_value_for(:birth_datetime).should == date_string end diff --git a/spec/validates_timeliness/helper_methods_spec.rb b/spec/validates_timeliness/helper_methods_spec.rb index 7d9af72..10ba504 100644 --- a/spec/validates_timeliness/helper_methods_spec.rb +++ b/spec/validates_timeliness/helper_methods_spec.rb @@ -22,7 +22,7 @@ describe ValidatesTimeliness::HelperMethods do describe ".timeliness_validated_attributes" do it 'should return attributes validated with plugin validator' do Person.validates_date :birth_date - Person.timeliness_validated_attributes.should == ["birth_date"] + Person.timeliness_validated_attributes.should == {"birth_date" => :date} end end end