aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel/lib')
-rw-r--r--activemodel/lib/active_model.rb1
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb43
-rw-r--r--activemodel/lib/active_model/dirty.rb112
-rw-r--r--activemodel/lib/active_model/validations/format.rb41
4 files changed, 176 insertions, 21 deletions
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index 9bb4cf8b54..b24a929ff5 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -29,6 +29,7 @@ module ActiveModel
autoload :AttributeMethods, 'active_model/attribute_methods'
autoload :Conversion, 'active_model/conversion'
autoload :DeprecatedErrorMethods, 'active_model/deprecated_error_methods'
+ autoload :Dirty, 'active_model/dirty'
autoload :Errors, 'active_model/errors'
autoload :Name, 'active_model/naming'
autoload :Naming, 'active_model/naming'
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index de80559036..1091ad3095 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -133,18 +133,31 @@ module ActiveModel
undefine_attribute_methods
end
+ def alias_attribute(new_name, old_name)
+ attribute_method_matchers.each do |matcher|
+ module_eval <<-STR, __FILE__, __LINE__+1
+ def #{matcher.method_name(new_name)}(*args)
+ send(:#{matcher.method_name(old_name)}, *args)
+ end
+ STR
+ end
+ 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}"
+ attr_names.each do |attr_name|
+ attribute_method_matchers.each do |matcher|
+ unless instance_method_already_implemented?(matcher.method_name(attr_name))
+ generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}"
if respond_to?(generate_method)
- send(generate_method, name)
+ send(generate_method, attr_name)
else
- generated_attribute_methods.module_eval("def #{method_name}(*args); send(:#{method.prefix}attribute#{method.suffix}, '#{name}', *args); end", __FILE__, __LINE__)
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__+1
+ def #{matcher.method_name(attr_name)}(*args)
+ send(:#{matcher.method_missing_target}, '#{attr_name}', *args)
+ end
+ STR
end
end
end
@@ -180,7 +193,7 @@ module ActiveModel
class AttributeMethodMatcher
attr_reader :prefix, :suffix
- AttributeMethodMatch = Struct.new(:prefix, :base, :suffix)
+ AttributeMethodMatch = Struct.new(:target, :attr_name)
def initialize(options = {})
options.symbolize_keys!
@@ -190,11 +203,19 @@ module ActiveModel
def match(method_name)
if matchdata = @regex.match(method_name)
- AttributeMethodMatch.new(matchdata[1], matchdata[2], matchdata[3])
+ AttributeMethodMatch.new(method_missing_target, matchdata[2])
else
nil
end
end
+
+ def method_name(attr_name)
+ "#{prefix}#{attr_name}#{suffix}"
+ end
+
+ def method_missing_target
+ :"#{prefix}attribute#{suffix}"
+ end
end
def attribute_method_matchers #:nodoc:
@@ -214,7 +235,7 @@ module ActiveModel
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)
+ return __send__(match.target, match.attr_name, *args, &block)
end
super
end
@@ -246,7 +267,7 @@ module ActiveModel
# 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)
+ if (match = method.match(method_name)) && attribute_method?(match.attr_name)
return match
end
end
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
new file mode 100644
index 0000000000..624c3647ca
--- /dev/null
+++ b/activemodel/lib/active_model/dirty.rb
@@ -0,0 +1,112 @@
+module ActiveModel
+ # Track unsaved attribute changes.
+ #
+ # A newly instantiated object is unchanged:
+ # person = Person.find_by_name('Uncle Bob')
+ # person.changed? # => false
+ #
+ # Change the name:
+ # person.name = 'Bob'
+ # person.changed? # => true
+ # person.name_changed? # => true
+ # person.name_was # => 'Uncle Bob'
+ # person.name_change # => ['Uncle Bob', 'Bob']
+ # person.name = 'Bill'
+ # person.name_change # => ['Uncle Bob', 'Bill']
+ #
+ # Save the changes:
+ # person.save
+ # person.changed? # => false
+ # person.name_changed? # => false
+ #
+ # Assigning the same value leaves the attribute unchanged:
+ # person.name = 'Bill'
+ # person.name_changed? # => false
+ # person.name_change # => nil
+ #
+ # Which attributes have changed?
+ # person.name = 'Bob'
+ # person.changed # => ['name']
+ # person.changes # => { 'name' => ['Bill', 'Bob'] }
+ #
+ # Resetting an attribute returns it to its original state:
+ # person.reset_name! # => 'Bill'
+ # person.changed? # => false
+ # person.name_changed? # => false
+ # person.name # => 'Bill'
+ #
+ # Before modifying an attribute in-place:
+ # person.name_will_change!
+ # person.name << 'y'
+ # person.name_change # => ['Bill', 'Billy']
+ module Dirty
+ extend ActiveSupport::Concern
+ include ActiveModel::AttributeMethods
+
+ included do
+ attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
+ attribute_method_affix :prefix => 'reset_', :suffix => '!'
+ end
+
+ # Do any attributes have unsaved changes?
+ # person.changed? # => false
+ # person.name = 'bob'
+ # person.changed? # => true
+ def changed?
+ !changed_attributes.empty?
+ end
+
+ # List of attributes with unsaved changes.
+ # person.changed # => []
+ # person.name = 'bob'
+ # person.changed # => ['name']
+ def changed
+ changed_attributes.keys
+ end
+
+ # Map of changed attrs => [original value, new value].
+ # person.changes # => {}
+ # person.name = 'bob'
+ # person.changes # => { 'name' => ['bill', 'bob'] }
+ def changes
+ changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
+ end
+
+ private
+ # Map of change <tt>attr => original value</tt>.
+ def changed_attributes
+ @changed_attributes ||= {}
+ end
+
+ # Handle <tt>*_changed?</tt> for +method_missing+.
+ def attribute_changed?(attr)
+ changed_attributes.include?(attr)
+ end
+
+ # Handle <tt>*_change</tt> for +method_missing+.
+ def attribute_change(attr)
+ [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
+ end
+
+ # Handle <tt>*_was</tt> for +method_missing+.
+ def attribute_was(attr)
+ attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
+ end
+
+ # Handle <tt>*_will_change!</tt> for +method_missing+.
+ def attribute_will_change!(attr)
+ begin
+ value = __send__(attr)
+ value = value.duplicable? ? value.clone : value
+ rescue TypeError, NoMethodError
+ end
+
+ changed_attributes[attr] = value
+ end
+
+ # Handle <tt>reset_*!</tt> for +method_missing+.
+ def reset_attribute!(attr)
+ __send__("#{attr}=", changed_attributes[attr]) if attribute_changed?(attr)
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb
index 6f3b668bf0..c670dafc7c 100644
--- a/activemodel/lib/active_model/validations/format.rb
+++ b/activemodel/lib/active_model/validations/format.rb
@@ -1,22 +1,30 @@
module ActiveModel
module Validations
module ClassMethods
- # Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression
- # provided.
+ # Validates whether the value of the specified attribute is of the correct form, going by the regular expression provided.
+ # You can require that the attribute matches the regular expression:
#
# class Person < ActiveRecord::Base
# validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
# end
#
+ # Alternatively, you can require that the specified attribute does _not_ match the regular expression:
+ #
+ # class Person < ActiveRecord::Base
+ # validates_format_of :email, :without => /NOSPAM/
+ # end
+ #
# Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the string, <tt>^</tt> and <tt>$</tt> match the start/end of a line.
#
- # A regular expression must be provided or else an exception will be raised.
+ # You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. In addition, both must be a regular expression,
+ # or else an exception will be raised.
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
- # * <tt>:with</tt> - The regular expression used to validate the format with (note: must be supplied!).
+ # * <tt>:with</tt> - Regular expression that if the attribute matches will result in a successful validation.
+ # * <tt>:without</tt> - Regular expression that if the attribute does not match will result in a successful validation.
# * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
@@ -25,14 +33,27 @@ module ActiveModel
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_format_of(*attr_names)
- configuration = { :with => nil }
- configuration.update(attr_names.extract_options!)
+ configuration = attr_names.extract_options!
+
+ unless configuration.include?(:with) ^ configuration.include?(:without) # ^ == xor, or "exclusive or"
+ raise ArgumentError, "Either :with or :without must be supplied (but not both)"
+ end
- raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
+ if configuration[:with] && !configuration[:with].is_a?(Regexp)
+ raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash"
+ end
- validates_each(attr_names, configuration) do |record, attr_name, value|
- unless value.to_s =~ configuration[:with]
- record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
+ if configuration[:without] && !configuration[:without].is_a?(Regexp)
+ raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash"
+ end
+
+ if configuration[:with]
+ validates_each(attr_names, configuration) do |record, attr_name, value|
+ record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) if value.to_s !~ configuration[:with]
+ end
+ elsif configuration[:without]
+ validates_each(attr_names, configuration) do |record, attr_name, value|
+ record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) if value.to_s =~ configuration[:without]
end
end
end