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|
# 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:

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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