aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel')
-rw-r--r--activemodel/lib/active_model.rb1
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb267
-rw-r--r--activemodel/lib/active_model/errors.rb6
-rw-r--r--activemodel/lib/active_model/state_machine.rb7
-rw-r--r--activemodel/lib/active_model/state_machine/event.rb12
-rw-r--r--activemodel/lib/active_model/state_machine/machine.rb29
-rw-r--r--activemodel/lib/active_model/state_machine/state_transition.rb2
-rw-r--r--activemodel/lib/active_model/validations.rb24
-rw-r--r--activemodel/test/cases/validations/presence_validation_test.rb14
-rw-r--r--activemodel/test/cases/validations_test.rb14
-rw-r--r--activemodel/test/models/custom_reader.rb17
11 files changed, 360 insertions, 33 deletions
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index 2de19597b1..9bb4cf8b54 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -26,6 +26,7 @@ $:.unshift(activesupport_path) if File.directory?(activesupport_path)
require 'active_support'
module ActiveModel
+ autoload :AttributeMethods, 'active_model/attribute_methods'
autoload :Conversion, 'active_model/conversion'
autoload :DeprecatedErrorMethods, 'active_model/deprecated_error_methods'
autoload :Errors, 'active_model/errors'
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
new file mode 100644
index 0000000000..de80559036
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -0,0 +1,267 @@
+module ActiveModel
+ class MissingAttributeError < NoMethodError
+ end
+
+ module AttributeMethods
+ extend ActiveSupport::Concern
+
+ # Declare and check for suffixed attribute methods.
+ module ClassMethods
+ # Defines an "attribute" method (like +inheritance_column+ or
+ # +table_name+). A new (class) method will be created with the
+ # given name. If a value is specified, the new method will
+ # return that value (as a string). Otherwise, the given block
+ # will be used to compute the value of the method.
+ #
+ # The original method will be aliased, with the new name being
+ # prefixed with "original_". This allows the new method to
+ # access the original value.
+ #
+ # Example:
+ #
+ # class A < ActiveRecord::Base
+ # define_attr_method :primary_key, "sysid"
+ # define_attr_method( :inheritance_column ) do
+ # original_inheritance_column + "_id"
+ # end
+ # end
+ def define_attr_method(name, value=nil, &block)
+ sing = metaclass
+ sing.send :alias_method, "original_#{name}", name
+ if block_given?
+ sing.send :define_method, name, &block
+ else
+ # use eval instead of a block to work around a memory leak in dev
+ # mode in fcgi
+ sing.class_eval "def #{name}; #{value.to_s.inspect}; end"
+ end
+ end
+
+ # Declares a method available for all attributes with the given prefix.
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
+ #
+ # #{prefix}#{attr}(*args, &block)
+ #
+ # to
+ #
+ # #{prefix}attribute(#{attr}, *args, &block)
+ #
+ # An <tt>#{prefix}attribute</tt> instance method must exist and accept at least
+ # the +attr+ argument.
+ #
+ # For example:
+ #
+ # class Person < ActiveRecord::Base
+ # attribute_method_prefix 'clear_'
+ #
+ # private
+ # def clear_attribute(attr)
+ # ...
+ # end
+ # end
+ #
+ # person = Person.find(1)
+ # person.name # => 'Gem'
+ # person.clear_name
+ # person.name # => ''
+ def attribute_method_prefix(*prefixes)
+ attribute_method_matchers.concat(prefixes.map { |prefix| AttributeMethodMatcher.new :prefix => prefix })
+ undefine_attribute_methods
+ end
+
+ # Declares a method available for all attributes with the given suffix.
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
+ #
+ # #{attr}#{suffix}(*args, &block)
+ #
+ # to
+ #
+ # attribute#{suffix}(#{attr}, *args, &block)
+ #
+ # An <tt>attribute#{suffix}</tt> instance method must exist and accept at least
+ # the +attr+ argument.
+ #
+ # For example:
+ #
+ # class Person < ActiveRecord::Base
+ # attribute_method_suffix '_short?'
+ #
+ # private
+ # def attribute_short?(attr)
+ # ...
+ # end
+ # end
+ #
+ # person = Person.find(1)
+ # person.name # => 'Gem'
+ # person.name_short? # => true
+ def attribute_method_suffix(*suffixes)
+ attribute_method_matchers.concat(suffixes.map { |suffix| AttributeMethodMatcher.new :suffix => suffix })
+ undefine_attribute_methods
+ end
+
+ # Declares a method available for all attributes with the given prefix
+ # and suffix. Uses +method_missing+ and <tt>respond_to?</tt> to rewrite
+ # the method.
+ #
+ # #{prefix}#{attr}#{suffix}(*args, &block)
+ #
+ # to
+ #
+ # #{prefix}attribute#{suffix}(#{attr}, *args, &block)
+ #
+ # An <tt>#{prefix}attribute#{suffix}</tt> instance method must exist and
+ # accept at least the +attr+ argument.
+ #
+ # For example:
+ #
+ # class Person < ActiveRecord::Base
+ # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!'
+ #
+ # private
+ # def reset_attribute_to_default!(attr)
+ # ...
+ # end
+ # end
+ #
+ # person = Person.find(1)
+ # person.name # => 'Gem'
+ # person.reset_name_to_default!
+ # person.name # => 'Gemma'
+ def attribute_method_affix(*affixes)
+ attribute_method_matchers.concat(affixes.map { |affix| AttributeMethodMatcher.new :prefix => affix[:prefix], :suffix => affix[:suffix] })
+ undefine_attribute_methods
+ end
+
+ def define_attribute_methods(attr_names)
+ return if attribute_methods_generated?
+ attr_names.each do |name|
+ attribute_method_matchers.each do |method|
+ method_name = "#{method.prefix}#{name}#{method.suffix}"
+ unless instance_method_already_implemented?(method_name)
+ generate_method = "define_method_#{method.prefix}attribute#{method.suffix}"
+
+ if respond_to?(generate_method)
+ send(generate_method, name)
+ else
+ generated_attribute_methods.module_eval("def #{method_name}(*args); send(:#{method.prefix}attribute#{method.suffix}, '#{name}', *args); end", __FILE__, __LINE__)
+ end
+ end
+ end
+ end
+ end
+
+ def undefine_attribute_methods
+ generated_attribute_methods.module_eval do
+ instance_methods.each { |m| undef_method(m) }
+ end
+ @attribute_methods_generated = nil
+ end
+
+ def generated_attribute_methods #:nodoc:
+ @generated_attribute_methods ||= begin
+ @attribute_methods_generated = true
+ mod = Module.new
+ include mod
+ mod
+ end
+ end
+
+ def attribute_methods_generated?
+ @attribute_methods_generated ? true : false
+ end
+
+ protected
+ def instance_method_already_implemented?(method_name)
+ method_defined?(method_name)
+ end
+
+ private
+ class AttributeMethodMatcher
+ attr_reader :prefix, :suffix
+
+ AttributeMethodMatch = Struct.new(:prefix, :base, :suffix)
+
+ def initialize(options = {})
+ options.symbolize_keys!
+ @prefix, @suffix = options[:prefix] || '', options[:suffix] || ''
+ @regex = /^(#{Regexp.escape(@prefix)})(.+?)(#{Regexp.escape(@suffix)})$/
+ end
+
+ def match(method_name)
+ if matchdata = @regex.match(method_name)
+ AttributeMethodMatch.new(matchdata[1], matchdata[2], matchdata[3])
+ else
+ nil
+ end
+ end
+ end
+
+ def attribute_method_matchers #:nodoc:
+ @@attribute_method_matchers ||= []
+ end
+ end
+
+ # Allows access to the object attributes, which are held in the <tt>@attributes</tt> hash, as though they
+ # were first-class methods. So a Person class with a name attribute can use Person#name and
+ # Person#name= and never directly use the attributes hash -- except for multiple assigns with
+ # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
+ # the completed attribute is not +nil+ or 0.
+ #
+ # It's also possible to instantiate related objects, so a Client class belonging to the clients
+ # table with a +master_id+ foreign key can instantiate master through Client#master.
+ def method_missing(method_id, *args, &block)
+ method_name = method_id.to_s
+ if match = match_attribute_method?(method_name)
+ guard_private_attribute_method!(method_name, args)
+ return __send__("#{match.prefix}attribute#{match.suffix}", match.base, *args, &block)
+ end
+ super
+ end
+
+ # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
+ # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
+ # which will all return +true+.
+ alias :respond_to_without_attributes? :respond_to?
+ def respond_to?(method, include_private_methods = false)
+ if super
+ return true
+ elsif !include_private_methods && super(method, true)
+ # If we're here then we haven't found among non-private methods
+ # but found among all methods. Which means that given method is private.
+ return false
+ elsif match_attribute_method?(method.to_s)
+ return true
+ end
+ super
+ end
+
+ protected
+ def attribute_method?(attr_name)
+ attributes.include?(attr_name)
+ end
+
+ private
+ # Returns a struct representing the matching attribute method.
+ # The struct's attributes are prefix, base and suffix.
+ def match_attribute_method?(method_name)
+ self.class.send(:attribute_method_matchers).each do |method|
+ if (match = method.match(method_name)) && attribute_method?(match.base)
+ return match
+ end
+ end
+ nil
+ end
+
+ # prevent method_missing from calling private methods with #send
+ def guard_private_attribute_method!(method_name, args)
+ if self.class.private_method_defined?(method_name)
+ raise NoMethodError.new("Attempt to call private method", method_name, args)
+ end
+ end
+
+ def missing_attribute(attr_name, stack)
+ raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index a4cf700231..7a3001174f 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -68,7 +68,7 @@ module ActiveModel
# Will add an error message to each of the attributes in +attributes+ that is empty.
def add_on_empty(attributes, custom_message = nil)
[attributes].flatten.each do |attribute|
- value = @base.send(attribute)
+ value = @base.send(:read_attribute_for_validation, attribute)
is_empty = value.respond_to?(:empty?) ? value.empty? : false
add(attribute, :empty, :default => custom_message) unless !value.nil? && !is_empty
end
@@ -77,7 +77,7 @@ module ActiveModel
# Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).
def add_on_blank(attributes, custom_message = nil)
[attributes].flatten.each do |attribute|
- value = @base.send(attribute)
+ value = @base.send(:read_attribute_for_validation, attribute)
add(attribute, :blank, :default => custom_message) if value.blank?
end
end
@@ -146,7 +146,7 @@ module ActiveModel
defaults = defaults.compact.flatten << :"messages.#{message}"
key = defaults.shift
- value = @base.send(attribute)
+ value = @base.send(:read_attribute_for_validation, attribute)
options = { :default => defaults,
:model => @base.class.name.humanize,
diff --git a/activemodel/lib/active_model/state_machine.rb b/activemodel/lib/active_model/state_machine.rb
index 1172d31ea3..527794b34d 100644
--- a/activemodel/lib/active_model/state_machine.rb
+++ b/activemodel/lib/active_model/state_machine.rb
@@ -5,12 +5,9 @@ module ActiveModel
autoload :State, 'active_model/state_machine/state'
autoload :StateTransition, 'active_model/state_machine/state_transition'
- class InvalidTransition < Exception
- end
+ extend ActiveSupport::Concern
- def self.included(base)
- require 'active_model/state_machine/machine'
- base.extend ClassMethods
+ class InvalidTransition < Exception
end
module ClassMethods
diff --git a/activemodel/lib/active_model/state_machine/event.rb b/activemodel/lib/active_model/state_machine/event.rb
index 3eb656b6d6..30e9601dc2 100644
--- a/activemodel/lib/active_model/state_machine/event.rb
+++ b/activemodel/lib/active_model/state_machine/event.rb
@@ -1,5 +1,3 @@
-require 'active_model/state_machine/state_transition'
-
module ActiveModel
module StateMachine
class Event
@@ -53,12 +51,12 @@ module ActiveModel
self
end
- private
- def transitions(trans_opts)
- Array(trans_opts[:from]).each do |s|
- @transitions << StateTransition.new(trans_opts.merge({:from => s.to_sym}))
+ private
+ def transitions(trans_opts)
+ Array(trans_opts[:from]).each do |s|
+ @transitions << StateTransition.new(trans_opts.merge({:from => s.to_sym}))
+ end
end
- end
end
end
end
diff --git a/activemodel/lib/active_model/state_machine/machine.rb b/activemodel/lib/active_model/state_machine/machine.rb
index a5ede021b1..777531213e 100644
--- a/activemodel/lib/active_model/state_machine/machine.rb
+++ b/activemodel/lib/active_model/state_machine/machine.rb
@@ -1,6 +1,3 @@
-require 'active_model/state_machine/state'
-require 'active_model/state_machine/event'
-
module ActiveModel
module StateMachine
class Machine
@@ -57,22 +54,22 @@ module ActiveModel
"@#{@name}_current_state"
end
- private
- def state(name, options = {})
- @states << (state_index[name] ||= State.new(name, :machine => self)).update(options)
- end
+ private
+ def state(name, options = {})
+ @states << (state_index[name] ||= State.new(name, :machine => self)).update(options)
+ end
- def event(name, options = {}, &block)
- (@events[name] ||= Event.new(self, name)).update(options, &block)
- end
+ def event(name, options = {}, &block)
+ (@events[name] ||= Event.new(self, name)).update(options, &block)
+ end
- def event_fired_callback
- @event_fired_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_fired'
- end
+ def event_fired_callback
+ @event_fired_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_fired'
+ end
- def event_failed_callback
- @event_failed_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_failed'
- end
+ def event_failed_callback
+ @event_failed_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_failed'
+ end
end
end
end
diff --git a/activemodel/lib/active_model/state_machine/state_transition.rb b/activemodel/lib/active_model/state_machine/state_transition.rb
index f9df998ea4..b0c5504de7 100644
--- a/activemodel/lib/active_model/state_machine/state_transition.rb
+++ b/activemodel/lib/active_model/state_machine/state_transition.rb
@@ -18,7 +18,7 @@ module ActiveModel
true
end
end
-
+
def execute(obj, *args)
case @on_transition
when Symbol, String
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index 54a869396d..7d49e60790 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -66,7 +66,7 @@ module ActiveModel
# Declare the validation.
send(validation_method(options[:on]), options) do |record|
attrs.each do |attr|
- value = record.send(attr)
+ value = record.send(:read_attribute_for_validation, attr)
next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
yield record, attr, value
end
@@ -95,6 +95,28 @@ module ActiveModel
def invalid?
!valid?
end
+
+ protected
+ # Hook method defining how an attribute value should be retieved. By default this is assumed
+ # to be an instance named after the attribute. Override this method in subclasses should you
+ # need to retrieve the value for a given attribute differently e.g.
+ # class MyClass
+ # include ActiveModel::Validations
+ #
+ # def initialize(data = {})
+ # @data = data
+ # end
+ #
+ # private
+ #
+ # def read_attribute_for_validation(key)
+ # @data[key]
+ # end
+ # end
+ #
+ def read_attribute_for_validation(key)
+ send(key)
+ end
end
end
diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb
index aa5bdf1e62..bb6fb91774 100644
--- a/activemodel/test/cases/validations/presence_validation_test.rb
+++ b/activemodel/test/cases/validations/presence_validation_test.rb
@@ -54,4 +54,18 @@ class PresenceValidationTest < ActiveModel::TestCase
assert p.valid?
end
end
+
+ def test_validates_presence_of_for_ruby_class_with_custom_reader
+ repair_validations(Person) do
+ CustomReader.validates_presence_of :karma
+
+ p = CustomReader.new
+ assert p.invalid?
+
+ assert_equal ["can't be blank"], p.errors[:karma]
+
+ p[:karma] = "Cold"
+ assert p.valid?
+ end
+ end
end
diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb
index 8c89494247..0b340e68bf 100644
--- a/activemodel/test/cases/validations_test.rb
+++ b/activemodel/test/cases/validations_test.rb
@@ -5,6 +5,7 @@ require 'cases/tests_database'
require 'models/topic'
require 'models/reply'
require 'models/developer'
+require 'models/custom_reader'
class ValidationsTest < ActiveModel::TestCase
include ActiveModel::TestsDatabase
@@ -97,6 +98,19 @@ class ValidationsTest < ActiveModel::TestCase
assert_equal %w(gotcha gotcha), t.errors[:title]
assert_equal %w(gotcha gotcha), t.errors[:content]
end
+
+ def test_validates_each_custom_reader
+ hits = 0
+ CustomReader.validates_each(:title, :content, [:title, :content]) do |record, attr|
+ record.errors.add attr, 'gotcha'
+ hits += 1
+ end
+ t = CustomReader.new("title" => "valid", "content" => "whatever")
+ assert !t.valid?
+ assert_equal 4, hits
+ assert_equal %w(gotcha gotcha), t.errors[:title]
+ assert_equal %w(gotcha gotcha), t.errors[:content]
+ end
def test_validate_block
Topic.validate { |topic| topic.errors.add("title", "will never be valid") }
diff --git a/activemodel/test/models/custom_reader.rb b/activemodel/test/models/custom_reader.rb
new file mode 100644
index 0000000000..7ac70e6167
--- /dev/null
+++ b/activemodel/test/models/custom_reader.rb
@@ -0,0 +1,17 @@
+class CustomReader
+ include ActiveModel::Validations
+
+ def initialize(data = {})
+ @data = data
+ end
+
+ def []=(key, value)
+ @data[key] = value
+ end
+
+ private
+
+ def read_attribute_for_validation(key)
+ @data[key]
+ end
+end \ No newline at end of file