diff options
Diffstat (limited to 'activemodel')
23 files changed, 343 insertions, 107 deletions
diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG index 9b7d2d026d..3d26d646b0 100644 --- a/activemodel/CHANGELOG +++ b/activemodel/CHANGELOG @@ -1,8 +1,10 @@ +* Add ability to define strict validation(with :strict => true option) that always raises exception when fails [Bogdan Gusiev] + * Deprecate "Model.model_name.partial_path" in favor of "model.to_partial_path" [Grant Hutchins, Peter Jaros] * Provide mass_assignment_sanitizer as an easy API to replace the sanitizer behavior. Also support both :logger (default) and :strict sanitizer behavior [Bogdan Gusiev] -*Rails 3.1.0 (unreleased)* +*Rails 3.1.0 (August 30, 2011)* * Alternate I18n namespace lookup is no longer supported. Instead of "activerecord.models.admins.post", do "activerecord.models.admins/post" instead [José Valim] diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 562f07fcd7..260ad01b65 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -19,5 +19,4 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('builder', '~> 3.0.0') s.add_dependency('i18n', '~> 0.6') - s.add_dependency('bcrypt-ruby', '~> 2.1.4') end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index bdc0eb4a0d..a201e983cd 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/class/attribute' +require 'active_support/deprecation' module ActiveModel class MissingAttributeError < NoMethodError @@ -60,7 +61,7 @@ module ActiveModel included do class_attribute :attribute_method_matchers, :instance_writer => false - self.attribute_method_matchers = [] + self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new] end module ClassMethods @@ -284,33 +285,25 @@ module ActiveModel def define_attribute_method(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}" + method_name = matcher.method_name(attr_name) + + unless instance_method_already_implemented?(method_name) + generate_method = "define_method_#{matcher.method_missing_target}" if respond_to?(generate_method) send(generate_method, attr_name) else - method_name = matcher.method_name(attr_name) + if method_name =~ COMPILABLE_REGEXP + defn = "def #{method_name}(*args)" + else + defn = "define_method(:'#{method_name}') do |*args|" + end generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 - if method_defined?('#{method_name}') - undef :'#{method_name}' + #{defn} + send(:#{matcher.method_missing_target}, '#{attr_name}', *args) end RUBY - - if method_name.to_s =~ COMPILABLE_REGEXP - generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{method_name}(*args) - send(:#{matcher.method_missing_target}, '#{attr_name}', *args) - end - RUBY - else - generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 - define_method('#{method_name}') do |*args| - send('#{matcher.method_missing_target}', '#{attr_name}', *args) - end - RUBY - end end end end @@ -336,7 +329,7 @@ module ActiveModel protected def instance_method_already_implemented?(method_name) - method_defined?(method_name) + generated_attribute_methods.method_defined?(method_name) end private @@ -357,8 +350,11 @@ module ActiveModel if attribute_method_matchers_cache.key?(method_name) attribute_method_matchers_cache[method_name] else + # Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix + # will match every time. + matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1) match = nil - attribute_method_matchers.detect { |method| match = method.match(method_name) } + matchers.detect { |method| match = method.match(method_name) } attribute_method_matchers_cache[method_name] = match end end @@ -366,10 +362,20 @@ module ActiveModel class AttributeMethodMatcher attr_reader :prefix, :suffix, :method_missing_target - AttributeMethodMatch = Struct.new(:target, :attr_name) + AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name) def initialize(options = {}) options.symbolize_keys! + + if options[:prefix] == '' || options[:suffix] == '' + ActiveSupport::Deprecation.warn( + "Specifying an empty prefix/suffix for an attribute method is no longer " \ + "necessary. If the un-prefixed/suffixed version of the method has not been " \ + "defined when `define_attribute_methods` is called, it will be defined " \ + "automatically." + ) + end + @prefix, @suffix = options[:prefix] || '', options[:suffix] || '' @regex = /^(#{Regexp.escape(@prefix)})(.+?)(#{Regexp.escape(@suffix)})$/ @method_missing_target = "#{@prefix}attribute#{@suffix}" @@ -378,7 +384,7 @@ module ActiveModel def match(method_name) if @regex =~ method_name - AttributeMethodMatch.new(method_missing_target, $2) + AttributeMethodMatch.new(method_missing_target, $2, method_name) else nil end @@ -387,6 +393,10 @@ module ActiveModel def method_name(attr_name) @method_name % attr_name end + + def plain? + prefix.empty? && suffix.empty? + end end end @@ -401,13 +411,21 @@ module ActiveModel # 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.target, match.attr_name, *args, &block) + def method_missing(method, *args, &block) + if respond_to_without_attributes?(method, true) + super + else + match = match_attribute_method?(method.to_s) + match ? attribute_missing(match, *args, &block) : super end - super + end + + # attribute_missing is like method_missing, but for attributes. When method_missing is + # called we check to see if there is a matching attribute method. If so, we call + # attribute_missing to dispatch the attribute. This method can be overloaded to + # customise the behaviour. + def attribute_missing(match, *args, &block) + __send__(match.target, match.attr_name, *args, &block) end # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>, @@ -416,15 +434,14 @@ module ActiveModel alias :respond_to_without_attributes? :respond_to? def respond_to?(method, include_private_methods = false) if super - return true + 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 the given method is private. - return false - elsif match_attribute_method?(method.to_s) - return true + false + else + !match_attribute_method?(method.to_s).nil? end - super end protected @@ -440,13 +457,6 @@ module ActiveModel match && attribute_method?(match.attr_name) ? match : 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}'", method_name, args) - end - end - def missing_attribute(attr_name, stack) raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index e3e71525fa..166cccf161 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -29,7 +29,7 @@ module ActiveModel # # include ActiveModel::Dirty # - # define_attribute_methods = [:name] + # define_attribute_methods [:name] # # def name # @name diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 36819553ee..d91e4a2b6a 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -63,7 +63,7 @@ module ActiveModel class Errors include Enumerable - CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank] + CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict] attr_reader :messages @@ -88,6 +88,7 @@ module ActiveModel def include?(error) (v = messages[error]) && v.any? end + alias :has_key? :include? # Get messages for +key+ def get(key) @@ -179,6 +180,7 @@ module ActiveModel all? { |k, v| v && v.empty? } end alias_method :blank?, :empty? + # Returns an xml formatted representation of the Errors hash. # # p.errors.add(:name, "can't be blank") @@ -218,6 +220,9 @@ module ActiveModel elsif message.is_a?(Proc) message = message.call end + if options[:strict] + raise ActiveModel::StrictValidationFailed, message + end self[attribute] << message end @@ -250,20 +255,22 @@ module ActiveModel # company.errors.full_messages # => # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"] def full_messages - map { |attribute, message| - if attribute == :base - message - else - attr_name = attribute.to_s.gsub('.', '_').humanize - attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) - - I18n.t(:"errors.format", { - :default => "%{attribute} %{message}", - :attribute => attr_name, - :message => message - }) - end - } + map { |attribute, message| full_message(attribute, message) } + end + + # Returns a full message for a given attribute. + # + # company.errors.full_message(:name, "is invalid") # => + # "Name is invalid" + def full_message(attribute, message) + return message if attribute == :base + attr_name = attribute.to_s.gsub('.', '_').humanize + attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) + I18n.t(:"errors.format", { + :default => "%{attribute} %{message}", + :attribute => attr_name, + :message => message + }) end # Translates an error message in its default scope @@ -319,4 +326,7 @@ module ActiveModel I18n.translate(key, options) end end + + class StrictValidationFailed < StandardError + end end diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 63380d6ffd..7a109d9a52 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -1,5 +1,3 @@ -require 'bcrypt' - module ActiveModel module SecurePassword extend ActiveSupport::Concern @@ -12,6 +10,10 @@ module ActiveModel # a "password_confirmation" attribute) are automatically added. # You can add more validations by hand if need be. # + # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use has_secure_password: + # + # gem 'bcrypt-ruby', '~> 3.0.0' + # # Example using Active Record (which automatically includes ActiveModel::SecurePassword): # # # Schema: User(name:string, password_digest:string) @@ -30,6 +32,11 @@ module ActiveModel # User.find_by_name("david").try(:authenticate, "notright") # => nil # User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user def has_secure_password + # Load bcrypt-ruby only when has_secured_password is used to avoid make ActiveModel + # (and by extension the entire framework) dependent on a binary library. + gem 'bcrypt-ruby', '~> 3.0.0' + require 'bcrypt' + attr_reader :password validates_confirmation_of :password diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 9260c5082d..b9f6f6cbbf 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -1,5 +1,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/array/wrap' + module ActiveModel # == Active Model Serialization diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 64dda3bcee..d61d9d7119 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -15,10 +15,10 @@ module ActiveModel class Attribute #:nodoc: attr_reader :name, :value, :type - def initialize(name, serializable, raw_value=nil) + def initialize(name, serializable, value) @name, @serializable = name, serializable - raw_value = raw_value.in_time_zone if raw_value.respond_to?(:in_time_zone) - @value = raw_value || @serializable.send(name) + value = value.in_time_zone if value.respond_to?(:in_time_zone) + @value = value @type = compute_type end @@ -49,40 +49,24 @@ module ActiveModel def initialize(serializable, options = nil) @serializable = serializable @options = options ? options.dup : {} - - @options[:only] = Array.wrap(@options[:only]).map { |n| n.to_s } - @options[:except] = Array.wrap(@options[:except]).map { |n| n.to_s } end - # To replicate the behavior in ActiveRecord#attributes, <tt>:except</tt> - # takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set - # for a N level model but is set for the N+1 level models, - # then because <tt>:except</tt> is set to a default value, the second - # level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if - # <tt>:only</tt> is set, always delete <tt>:except</tt>. - def attributes_hash - attributes = @serializable.attributes - if options[:only].any? - attributes.slice(*options[:only]) - elsif options[:except].any? - attributes.except(*options[:except]) - else - attributes - end + def serializable_hash + @serializable.serializable_hash(@options.except(:include)) end - def serializable_attributes - attributes_hash.map do |name, value| - self.class::Attribute.new(name, @serializable, value) + def serializable_collection + methods = Array.wrap(options[:methods]).map(&:to_s) + serializable_hash.map do |name, value| + name = name.to_s + if methods.include?(name) + self.class::MethodAttribute.new(name, @serializable, value) + else + self.class::Attribute.new(name, @serializable, value) + end end end - def serializable_methods - Array.wrap(options[:methods]).map do |name| - self.class::MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s) - end.compact - end - def serialize require 'builder' unless defined? ::Builder @@ -114,7 +98,7 @@ module ActiveModel end def add_attributes_and_methods - (serializable_attributes + serializable_methods).each do |attribute| + serializable_collection.each do |attribute| key = ActiveSupport::XmlMini.rename_key(attribute.name, options) ActiveSupport::XmlMini.to_tag(key, attribute.value, options.merge(attribute.decorations)) diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 01907ac9da..e628c6f306 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -58,6 +58,8 @@ module ActiveModel # <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. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information def validates_acceptance_of(*attr_names) validates_with AcceptanceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index a9dcb0b505..6573a7d264 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -58,6 +58,8 @@ module ActiveModel # <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. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information def validates_confirmation_of(*attr_names) validates_with ConfirmationValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index d3b8d31502..644cc814a7 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -59,6 +59,8 @@ module ActiveModel # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should # 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. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information def validates_exclusion_of(*attr_names) validates_with ExclusionValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index 090e8cfbae..d3faa8c6a6 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -84,6 +84,8 @@ module ActiveModel # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should # 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. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information def validates_format_of(*attr_names) validates_with FormatValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 9a9270d615..147e2ecb69 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -59,6 +59,8 @@ module ActiveModel # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should # 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. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information def validates_inclusion_of(*attr_names) validates_with InclusionValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 144e73904e..eb7aac709d 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -96,6 +96,8 @@ module ActiveModel # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to # count words as in above example.) # Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information def validates_length_of(*attr_names) validates_with LengthValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 0d1903362c..34d447a0fa 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -107,6 +107,8 @@ module ActiveModel # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should # 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. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information # # The following checks can also be supplied with a proc or a symbol which corresponds to a method: # * <tt>:greater_than</tt> diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index cfb4c33dcc..35af7152db 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -35,6 +35,8 @@ module ActiveModel # * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should # 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. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information # def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 7ff42de00b..b85c2453fb 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -70,8 +70,8 @@ module ActiveModel # validator's initializer as +options[:in]+ while other types including # regular expressions and strings are passed as +options[:with]+ # - # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+ and +:allow_nil+ can be given - # to one specific validator, as a hash: + # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+ and +:strict+ + # can be given to one specific validator, as a hash: # # validates :password, :presence => { :if => :password_required? }, :confirmation => true # @@ -101,12 +101,24 @@ module ActiveModel end end + # This method is used to define validation that can not be corrected by end user + # and is considered exceptional. + # So each validator defined with bang or <tt>:strict</tt> option set to <tt>true</tt> + # will always raise <tt>ActiveModel::InternalValidationFailed</tt> instead of adding error + # when validation fails + # See <tt>validates</tt> for more information about validation itself. + def validates!(*attributes) + options = attributes.extract_options! + options[:strict] = true + validates(*(attributes << options)) + end + protected # When creating custom validators, it might be useful to be able to specify # additional default keys. This can be done by overwriting this method. def _validates_default_keys - [ :if, :unless, :on, :allow_blank, :allow_nil ] + [ :if, :unless, :on, :allow_blank, :allow_nil , :strict] end def _parse_validates_options(options) #:nodoc: diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index a87b213fe4..83aae206a6 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -61,7 +61,9 @@ module ActiveModel # (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. - # + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information + # If you pass any additional configuration options, they will be passed # to the class and available as <tt>options</tt>: # @@ -140,4 +142,4 @@ module ActiveModel end end end -end
\ No newline at end of file +end diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index 9840e3364c..67471ed497 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -3,8 +3,6 @@ require 'cases/helper' class ModelWithAttributes include ActiveModel::AttributeMethods - attribute_method_suffix '' - class << self define_method(:bar) do 'original bar' @@ -24,14 +22,31 @@ end class ModelWithAttributes2 include ActiveModel::AttributeMethods + attr_accessor :attributes + attribute_method_suffix '_test' + +private + def attribute(name) + attributes[name.to_s] + end + + alias attribute_test attribute + + def private_method + "<3 <3" + end + +protected + + def protected_method + "O_o O_o" + end end class ModelWithAttributesWithSpaces include ActiveModel::AttributeMethods - attribute_method_suffix '' - def attributes { :'foo bar' => 'value of foo bar'} end @@ -45,8 +60,6 @@ end class ModelWithWeirdNamesAttributes include ActiveModel::AttributeMethods - attribute_method_suffix '' - class << self define_method(:'c?d') do 'original c?d' @@ -76,6 +89,29 @@ class AttributeMethodsTest < ActiveModel::TestCase assert_equal "value of foo", ModelWithAttributes.new.foo end + test '#define_attribute_method does not generate attribute method if already defined in attribute module' do + klass = Class.new(ModelWithAttributes) + klass.generated_attribute_methods.module_eval do + def foo + '<3' + end + end + klass.define_attribute_method(:foo) + + assert_equal '<3', klass.new.foo + end + + test '#define_attribute_method generates a method that is already defined on the host' do + klass = Class.new(ModelWithAttributes) do + def foo + super + end + end + klass.define_attribute_method(:foo) + + assert_equal 'value of foo', klass.new.foo + end + test '#define_attribute_method generates attribute method with invalid identifier characters' do ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b') @@ -129,4 +165,64 @@ class AttributeMethodsTest < ActiveModel::TestCase assert !ModelWithAttributes.new.respond_to?(:foo) assert_raises(NoMethodError) { ModelWithAttributes.new.foo } end + + test 'acessing a suffixed attribute' do + m = ModelWithAttributes2.new + m.attributes = { 'foo' => 'bar' } + + assert_equal 'bar', m.foo + assert_equal 'bar', m.foo_test + end + + test 'explicitly specifying an empty prefix/suffix is deprecated' do + klass = Class.new(ModelWithAttributes) + + assert_deprecated { klass.attribute_method_suffix '' } + assert_deprecated { klass.attribute_method_prefix '' } + + klass.define_attribute_methods([:foo]) + + assert_equal 'value of foo', klass.new.foo + end + + test 'should not interfere with method_missing if the attr has a private/protected method' do + m = ModelWithAttributes2.new + m.attributes = { 'private_method' => '<3', 'protected_method' => 'O_o' } + + # dispatches to the *method*, not the attribute + assert_equal '<3 <3', m.send(:private_method) + assert_equal 'O_o O_o', m.send(:protected_method) + + # sees that a method is already defined, so doesn't intervene + assert_raises(NoMethodError) { m.private_method } + assert_raises(NoMethodError) { m.protected_method } + end + + test 'should not interfere with respond_to? if the attribute has a private/protected method' do + m = ModelWithAttributes2.new + m.attributes = { 'private_method' => '<3', 'protected_method' => 'O_o' } + + assert !m.respond_to?(:private_method) + assert m.respond_to?(:private_method, true) + + # This is messed up, but it's how Ruby works at the moment. Apparently it will be changed + # in the future. + assert m.respond_to?(:protected_method) + assert m.respond_to?(:protected_method, true) + end + + test 'should use attribute_missing to dispatch a missing attribute' do + m = ModelWithAttributes2.new + m.attributes = { 'foo' => 'bar' } + + def m.attribute_missing(match, *args, &block) + match + end + + match = m.foo_test + + assert_equal 'foo', match.attr_name + assert_equal 'attribute_test', match.target + assert_equal 'foo_test', match.method_name + end end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 85ca8ca835..4c76bb43a8 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -33,6 +33,12 @@ class ErrorsTest < ActiveModel::TestCase assert errors.include?(:foo), 'errors should include :foo' end + def test_has_key? + errors = ActiveModel::Errors.new(self) + errors[:foo] = 'omg' + assert errors.has_key?(:foo), 'errors should have key :foo' + end + test "should return true if no errors" do person = Person.new person.errors[:foo] @@ -46,7 +52,6 @@ class ErrorsTest < ActiveModel::TestCase person.validate! assert_equal ["name can not be nil"], person.errors.full_messages assert_equal ["can not be nil"], person.errors[:name] - end test 'should be able to assign error' do @@ -72,7 +77,6 @@ class ErrorsTest < ActiveModel::TestCase person.errors.add(:name, "can not be blank") person.errors.add(:name, "can not be nil") assert_equal ["name can not be blank", "name can not be nil"], person.errors.to_a - end test 'to_hash should return an ordered hash' do @@ -80,4 +84,33 @@ class ErrorsTest < ActiveModel::TestCase person.errors.add(:name, "can not be blank") assert_instance_of ActiveSupport::OrderedHash, person.errors.to_hash end + + test 'full_messages should return an array of error messages, with the attribute name included' do + person = Person.new + person.errors.add(:name, "can not be blank") + person.errors.add(:name, "can not be nil") + assert_equal ["name can not be blank", "name can not be nil"], person.errors.to_a + end + + test 'full_message should return the given message if attribute equals :base' do + person = Person.new + assert_equal "press the button", person.errors.full_message(:base, "press the button") + end + + test 'full_message should return the given message with the attribute name included' do + person = Person.new + assert_equal "name can not be blank", person.errors.full_message(:name, "can not be blank") + end + + test 'should return a JSON hash representation of the errors' do + person = Person.new + person.errors.add(:name, "can not be blank") + person.errors.add(:name, "can not be nil") + person.errors.add(:email, "is invalid") + hash = person.errors.as_json + assert_equal ["can not be blank", "can not be nil"], hash[:name] + assert_equal ["is invalid"], hash[:email] + end + end + diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb index 5122f08eec..071cb9ff4e 100644 --- a/activemodel/test/cases/serialization_test.rb +++ b/activemodel/test/cases/serialization_test.rb @@ -114,8 +114,21 @@ class SerializationTest < ActiveModel::TestCase @user.friends.first.friends = [@user] expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", :friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male', - :friends => ["email"=>"david@example.com", "gender"=>"male", "name"=>"David"]}, + :friends => [{"email"=>"david@example.com", "gender"=>"male", "name"=>"David"}]}, {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female', :friends => []}]} assert_equal expected , @user.serializable_hash(:include => {:friends => {:include => :friends}}) end + + def test_only_include + expected = {"name"=>"David", :friends => [{"name" => "Joe"}, {"name" => "Sue"}]} + assert_equal expected , @user.serializable_hash(:only => :name, :include => {:friends => {:only => :name}}) + end + + def test_except_include + expected = {"name"=>"David", "email"=>"david@example.com", + :friends => [{"name" => 'Joe', "email" => 'joe@example.com'}, + {"name" => "Sue", "email" => 'sue@example.com'}]} + assert_equal expected , @user.serializable_hash(:except => :gender, :include => {:friends => {:except => :gender}}) + end + end diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index a38ef8e223..fc73d9dcd8 100644 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb @@ -33,6 +33,12 @@ class Address end end +class SerializableContact < Contact + def serializable_hash(options={}) + super(options.merge(:only => [:name, :age])) + end +end + class XmlSerializationTest < ActiveModel::TestCase def setup @contact = Contact.new @@ -96,6 +102,17 @@ class XmlSerializationTest < ActiveModel::TestCase assert_match %r{<createdAt}, @xml end + test "should use serialiable hash" do + @contact = SerializableContact.new + @contact.name = 'aaron stack' + @contact.age = 25 + + @xml = @contact.to_xml + assert_match %r{<name>aaron stack</name>}, @xml + assert_match %r{<age type="integer">25</age>}, @xml + assert_no_match %r{<awesome>}, @xml + end + test "should allow skipped types" do @xml = @contact.to_xml :skip_types => true assert_match %r{<age>25</age>}, @xml diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 0b50acf913..2f4376bd41 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -297,4 +297,37 @@ class ValidationsTest < ActiveModel::TestCase assert auto.valid? end + + def test_strict_validation_in_validates + Topic.validates :title, :strict => true, :presence => true + assert_raises ActiveModel::StrictValidationFailed do + Topic.new.valid? + end + end + + def test_strict_validation_not_fails + Topic.validates :title, :strict => true, :presence => true + assert Topic.new(:title => "hello").valid? + end + + def test_strict_validation_particular_validator + Topic.validates :title, :presence => {:strict => true} + assert_raises ActiveModel::StrictValidationFailed do + Topic.new.valid? + end + end + + def test_strict_validation_in_custom_validator_helper + Topic.validates_presence_of :title, :strict => true + assert_raises ActiveModel::StrictValidationFailed do + Topic.new.valid? + end + end + + def test_validates_with_bang + Topic.validates! :title, :presence => true + assert_raises ActiveModel::StrictValidationFailed do + Topic.new.valid? + end + end end |