{"action":"closed","issue":{"url":"https://api.github.com/repos/rails/rails/issues/17547","labels_url":"https://api.github.com/repos/rails/rails/issues/17547/labels{/name}","comments_url":"https://api.github.com/repos/rails/rails/issues/17547/comments","events_url":"https://api.github.com/repos/rails/rails/issues/17547/events","html_url":"https://github.com/rails/rails/issues/17547","id":48029491,"number":17547,"title":"Special handling when options[:message] is a Proc in #normalize_message.","user":{"login":"nengxu","id":23847,"avatar_url":"https://avatars.githubusercontent.com/u/23847?v=3","gravatar_id":"","url":"https://api.github.com/users/nengxu","html_url":"https://github.com/nengxu","followers_url":"https://api.github.com/users/nengxu/followers","following_url":"https://api.github.com/users/nengxu/following{/other_user}","gists_url":"https://api.github.com/users/nengxu/gists{/gist_id}","starred_url":"https://api.github.com/users/nengxu/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nengxu/subscriptions","organizations_url":"https://api.github.com/users/nengxu/orgs","repos_url":"https://api.github.com/users/nengxu/repos","events_url":"https://api.github.com/users/nengxu/events{/privacy}","received_events_url":"https://api.github.com/users/nengxu/received_events","type":"User","site_admin":false},"labels":[],"state":"closed","locked":false,"assignee":null,"milestone":null,"comments":6,"created_at":"2014-11-06T23:34:37Z","updated_at":"2015-01-22T18:01:10Z","closed_at":"2015-01-22T18:01:10Z","body":"# Background\r\n\r\nIn our project, we are using the standard \"validates_inclusion_of\" ActiveRecord validation, but with custom error message which needs to be localized. For example:\r\n\r\n    validates_inclusion_of :priority, :in => 1..999, :message => validate_inclusion_message('1-999')\r\n\r\nIn order to dynamically localize the error message at run time, a Proc is provided as the :message parameter:\r\n\r\n    def self.validate_inclusion_message(range)\r\n      lambda {|| I18n.t(:range_error, default: \"Valid Range %{range}\", range: range)}\r\n    end\r\n\r\nHowever, I18n will raise an error \"wrong number of arguments (2 for 0)\", with the trace back:\r\n\r\n     ArgumentError - wrong number of arguments (2 for 0):\r\n      i18n (0.6.9) lib/i18n/backend/base.rb:122:in `block in resolve'\r\n      i18n (0.6.9) lib/i18n/backend/base.rb:116:in `resolve'\r\n      i18n (0.6.9) lib/i18n/backend/base.rb:103:in `block in default'\r\n      i18n (0.6.9) lib/i18n/backend/base.rb:102:in `default'\r\n      i18n (0.6.9) lib/i18n/backend/base.rb:34:in `translate'\r\n      i18n (0.6.9) lib/i18n.rb:157:in `block in translate'\r\n      i18n (0.6.9) lib/i18n.rb:153:in `translate'\r\n      activemodel (4.0.3) lib/active_model/errors.rb:435:in `generate_message'\r\n      activemodel (4.0.3) lib/active_model/errors.rb:444:in `normalize_message'\r\n      activemodel (4.0.3) lib/active_model/errors.rb:293:in `add'\r\n      activemodel (4.0.3) lib/active_model/validations/inclusion.rb:11:in `validate_each'\r\n      activemodel (4.0.3) lib/active_model/validator.rb:153:in `block in validate'\r\n      activemodel (4.0.3) lib/active_model/validator.rb:150:in `validate'\r\n\r\nAfter investigation, I found that the error is caused by ActiveModel::Validations::InclusionValidator\r\n\r\n    module ActiveModel\r\n\r\n      module Validations\r\n        class InclusionValidator < EachValidator # :nodoc:\r\n          include Clusivity\r\n\r\n          def validate_each(record, attribute, value)\r\n            unless include?(record, value)\r\n              record.errors.add(attribute, :inclusion, options.except(:in, :within).merge!(:value => value))\r\n            end\r\n          end\r\n        end\r\n\r\nAlthough we had passed a custom :message option, the default \":inclusion\" was still passed to errors.add as the :message parameter, as in active_model/errors.rb:\r\n\r\n    def add(attribute, message = nil, options = {})\r\n      message = normalize_message(attribute, message, options)\r\n      if exception = options[:strict]\r\n        exception = ActiveModel::StrictValidationFailed if exception == true\r\n        raise exception, full_message(attribute, message)\r\n      end\r\n\r\n      self[attribute] << message\r\n    end\r\n\r\nHence when in #normalize_message, the path of generate_message was chosen:\r\n\r\n    def normalize_message(attribute, message, options)\r\n      message ||= :invalid\r\n\r\n      case message\r\n      when Symbol\r\n        generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))\r\n      when Proc\r\n        message.call\r\n      else\r\n        message\r\n      end\r\n    end\r\n\r\nThen in #generate_message, \r\n\r\n    def generate_message(attribute, type = :invalid, options = {})\r\n      type = options.delete(:message) if options[:message].is_a?(Symbol)\r\n\r\n      if @base.class.respond_to?(:i18n_scope)\r\n        defaults = @base.class.lookup_ancestors.map do |klass|\r\n          [ :\"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}\",\r\n            :\"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}\" ]\r\n        end\r\n      else\r\n        defaults = []\r\n      end\r\n\r\n      defaults << options.delete(:message)\r\n      defaults << :\"#{@base.class.i18n_scope}.errors.messages.#{type}\" if @base.class.respond_to?(:i18n_scope)\r\n      defaults << :\"errors.attributes.#{attribute}.#{type}\"\r\n      defaults << :\"errors.messages.#{type}\"\r\n\r\n      defaults.compact!\r\n      defaults.flatten!\r\n\r\n      key = defaults.shift\r\n      value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)\r\n\r\n      options = {\r\n        :default => defaults,\r\n        :model => @base.class.model_name.human,\r\n        :attribute => @base.class.human_attribute_name(attribute),\r\n        :value => value\r\n      }.merge!(options)\r\n\r\n      I18n.translate(key, options)\r\n    end\r\n\r\nOur Proc was passed in as an element of the :default parameter to I18n.translate, by this line of code:\r\n\r\n      defaults << options.delete(:message)\r\n\r\nSo in lib/i18n/backend/base.rb:122, I18n raised the error when it was processing our Proc as one element of :default\r\n\r\n              resolve(locale, object, subject.call(date_or_time, options))\r\n\r\nWhere \"subject\" is our Proc, which by chance takes no parameter. Hence the 2 for 0 error.\r\n\r\n# Analysis (updated: please see comments below)\r\n\r\nIn our case, the error was detected by the chance that our Proc takes no parameter. However, the behavior of ActiveModel::Validations::InclusionValidator in the following line is not reasonable, when a custom message has been specified:\r\n\r\n              record.errors.add(attribute, :inclusion, options.except(:in, :within).merge!(:value => value))\r\n\r\nNo matter whether we specify a normal string, or a Proc as the custom message in the form of the :message option, this custom message should be passed to errors.add as the :message parameter. Not the default :inclusion. In other words, the code should be changed to:\r\n\r\n          # check custom message\r\n          if options[:message]\r\n            record.errors.add(attribute, options[:message], options.except(:in, :within, :message).merge!(:value => value))\r\n          else\r\n            record.errors.add(attribute, :inclusion, options.except(:in, :within).merge!(:value => value))\r\n          end\r\n\r\nThen as in our case of a Proc passed in as the :message option, the right path would be chosen in #normalize_message:\r\n\r\n      when Proc\r\n        message.call\r\n\r\nWhen a symbol is passed in as the :message option, it will go through the #generate_message route correctly.\r\n\r\nWhen a string is passed in as the :message option, it will go through the other route correctly.\r\n\r\n      else\r\n        message\r\n\r\nSo in all scenarios, the custom message will be used correctly.\r\n\r\n# Solution\r\n\r\nI will submit a Pull Request for the fix soon.\r\n\r\n\r\n\r\n\r\n"}}