change ORM attribute generation and extension mechanism

now using shim since the attribute matcher is not required for AM
This commit is contained in:
Adam Meehan 2010-09-16 22:33:22 +10:00
parent d0080ebac4
commit 9ddd150b2f
11 changed files with 179 additions and 81 deletions

View File

@ -42,13 +42,18 @@ This creates configuration initializer and locale files. In the initializer, you
ValidatesTimeliness.setup do |config| ValidatesTimeliness.setup do |config|
# Add validation helpers to these classes # Add plugin to supported ORMs (only :active_record for now)
# config.extend_classes = [ ActiveRecord::Base ] # config.extend_orms = [ :active_record ]
end 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. By default the plugin extends ActiveRecord if present. Currently ActiveRecord is the only ORM included for extension. If you wish to extend
As long as the ORM supports ActiveModel validations, it should work. 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: == Usage:

View File

@ -1,6 +1,6 @@
ValidatesTimeliness.setup do |config| ValidatesTimeliness.setup do |config|
# Add validation helpers to these classes # Add plugin to supported ORMs (only :active_record for now)
# config.extend_classes = [ ActiveRecord::Base ] # config.extend_orms = [ :active_record ]
# #
# Set the dummy date part for a time type values. # Set the dummy date part for a time type values.
# config.dummy_date_for_time_type = [ 2000, 1, 1 ] # config.dummy_date_for_time_type = [ 2000, 1, 1 ]

View File

@ -15,9 +15,9 @@ require 'active_support/core_ext/date_time/zones'
module ValidatesTimeliness module ValidatesTimeliness
autoload :VERSION, 'validates_timeliness/version' autoload :VERSION, 'validates_timeliness/version'
# Add validation helpers to these classes # Add plugin to supported ORMs (only :active_record for now)
mattr_accessor :extend_classes mattr_accessor :extend_orms
@@extend_classes = [ defined?(ActiveRecord) && ActiveRecord::Base ].compact @@extend_orms = [ defined?(ActiveRecord) && :active_record ].compact
# Set the dummy date part for a time type values. # Set the dummy date part for a time type values.
mattr_accessor :dummy_date_for_time_type mattr_accessor :dummy_date_for_time_type
@ -37,10 +37,7 @@ module ValidatesTimeliness
# Setup method for plugin configuration # Setup method for plugin configuration
def self.setup def self.setup
yield self yield self
extend_classes.each {|klass| extend_orms.each {|orm| require "validates_timeliness/orms/#{orm}" }
klass.send(:include, ValidatesTimeliness::HelperMethods)
klass.send(:include, ValidatesTimeliness::AttributeMethods)
}
end end
end end

View File

@ -2,28 +2,39 @@ module ValidatesTimeliness
module AttributeMethods module AttributeMethods
extend ActiveSupport::Concern 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 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 protected
def define_method_attribute=(attr_name) def define_timeliness_write_method(attr_name, type, timezone_aware)
if timeliness_validated_attributes.include?(attr_name) method_body, line = <<-EOV, __LINE__ + 1
method_body, line = <<-EOV, __LINE__ + 1 def #{attr_name}=(value)
def #{attr_name}=(value) @attributes_cache ||= {}
@attributes_cache ||= {} @attributes_cache["_#{attr_name}_before_type_cast"] = value
@attributes_cache["_#{attr_name}_before_type_cast"] = value super
super end
end EOV
EOV class_eval(method_body, __FILE__, line)
class_eval(method_body, __FILE__, line) end
end
super rescue(NoMethodError) 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
end end
@ -36,22 +47,5 @@ module ValidatesTimeliness
end 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
end end

View File

@ -5,30 +5,37 @@ module ValidatesTimeliness
included do included do
include ValidationMethods include ValidationMethods
extend ValidationMethods extend ValidationMethods
class_inheritable_accessor :timeliness_validated_attributes
self.timeliness_validated_attributes = {}
end end
module ValidationMethods 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) 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 end
def validates_time(*attr_names) 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 end
def validates_datetime(*attr_names) 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
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 end
end end

View File

@ -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

View File

@ -18,9 +18,13 @@ module ValidatesTimeliness
:timeliness :timeliness
end end
def setup(klass)
@klass = klass
end
def initialize(options) def initialize(options)
@allow_nil, @allow_blank = options.delete(:allow_nil), options.delete(:allow_blank)
@type = options.delete(:type) || :datetime @type = options.delete(:type) || :datetime
@allow_nil, @allow_blank = options.delete(:allow_nil), options.delete(:allow_blank)
@restrictions_to_check = RESTRICTIONS.keys & options.keys @restrictions_to_check = RESTRICTIONS.keys & options.keys
if range = options.delete(:between) if range = options.delete(:between)

View File

@ -13,9 +13,10 @@ require 'rspec_tag_matchers'
require 'model_helpers' require 'model_helpers'
require 'validates_timeliness' require 'validates_timeliness'
require 'test_model'
ValidatesTimeliness.setup do |c| ValidatesTimeliness.setup do |c|
c.extend_classes = [ ActiveModel::Validations, ActiveRecord::Base ] c.extend_orms = [ :active_record ]
c.enable_date_time_select_extension! c.enable_date_time_select_extension!
c.enable_multiparameter_extension! c.enable_multiparameter_extension!
end end
@ -25,22 +26,38 @@ Time.zone = 'Australia/Melbourne'
LOCALE_PATH = File.expand_path(File.dirname(__FILE__) + '/../lib/generators/validates_timeliness/templates/en.yml') LOCALE_PATH = File.expand_path(File.dirname(__FILE__) + '/../lib/generators/validates_timeliness/templates/en.yml')
I18n.load_path.unshift(LOCALE_PATH) I18n.load_path.unshift(LOCALE_PATH)
class Person # Extend TestModel as you would another ORM/ODM module
include ActiveModel::AttributeMethods module TestModel
include ActiveModel::Validations include ValidatesTimeliness::HelperMethods
extend ActiveModel::Translation include ValidatesTimeliness::AttributeMethods
attr_accessor :birth_date, :birth_time, :birth_datetime def self.included(base)
attr_accessor :attributes base.extend HookMethods
end
def initialize(attributes = {}) module HookMethods
@attributes = {} # Hook method for attribute method generation
attributes.each do |key, value| def define_attribute_methods(attr_names)
send "#{key}=", value 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 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::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'})
ActiveRecord::Migration.verbose = false ActiveRecord::Migration.verbose = false
ActiveRecord::Schema.define(:version => 1) do ActiveRecord::Schema.define(:version => 1) do
@ -54,6 +71,10 @@ ActiveRecord::Schema.define(:version => 1) do
end end
class Employee < ActiveRecord::Base class Employee < ActiveRecord::Base
validates_date :birth_date
validates_time :birth_time
validates_datetime :birth_datetime
define_attribute_methods
end end
Rspec.configure do |c| Rspec.configure do |c|
@ -61,8 +82,10 @@ Rspec.configure do |c|
c.include(RspecTagMatchers) c.include(RspecTagMatchers)
c.before do c.before do
Person.reset_callbacks(:validate) Person.reset_callbacks(:validate)
Person.timeliness_validated_attributes = {}
Person._validators.clear Person._validators.clear
Employee.reset_callbacks(:validate) Employee.reset_callbacks(:validate)
Employee.timeliness_validated_attributes = {}
Employee._validators.clear Employee._validators.clear
end end
end end

56
spec/test_model.rb Normal file
View File

@ -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

View File

@ -1,20 +1,18 @@
require 'spec_helper' require 'spec_helper'
describe ValidatesTimeliness::AttributeMethods do 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 it 'should define _timeliness_raw_value_for instance method' do
Person.instance_methods.should include('_timeliness_raw_value_for') Person.instance_methods.should include('_timeliness_raw_value_for')
end end
context "attribute write method" do 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 it 'should cache attribute raw value' do
r = Employee.new r = EmployeeCopy.new
r.birth_datetime = date_string = '2010-01-01' r.birth_datetime = date_string = '2010-01-01'
r._timeliness_raw_value_for(:birth_datetime).should == date_string r._timeliness_raw_value_for(:birth_datetime).should == date_string
end end

View File

@ -22,7 +22,7 @@ describe ValidatesTimeliness::HelperMethods do
describe ".timeliness_validated_attributes" do describe ".timeliness_validated_attributes" do
it 'should return attributes validated with plugin validator' do it 'should return attributes validated with plugin validator' do
Person.validates_date :birth_date Person.validates_date :birth_date
Person.timeliness_validated_attributes.should == ["birth_date"] Person.timeliness_validated_attributes.should == {"birth_date" => :date}
end end
end end
end end