From d424a813509d2e2db756aef26a024eb9f327e37d Mon Sep 17 00:00:00 2001 From: Alexandre Barret Date: Wed, 30 Jun 2021 17:36:41 +1200 Subject: [PATCH 1/5] Create message_format & full_message validation option Add the ability to change the message_format on ActiveModel::Validations full_messages. Like error.add(:base, 'message') but for any attribute. Examples: class Person include ActiveModel::Model validates :name, presence: { full_message: 'Person requires a name' } end OR class Person include ActiveModel::Model validates :name, presence: { message: 'Person requires a name', message_format: "%{message}" } end This would result in person = Person.new(name: nil) person.valid? puts person.errors.full_messages => ['Person requires a name'] --- activemodel/lib/active_model/error.rb | 21 ++++++++++++++++----- activemodel/lib/active_model/errors.rb | 4 ++-- activemodel/test/cases/error_test.rb | 26 +++++++++++++++++++++++++- activemodel/test/cases/errors_test.rb | 6 ++++++ 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/activemodel/lib/active_model/error.rb b/activemodel/lib/active_model/error.rb index 103d7d6e7144e..6ef9e343851c6 100644 --- a/activemodel/lib/active_model/error.rb +++ b/activemodel/lib/active_model/error.rb @@ -8,11 +8,11 @@ module ActiveModel # Represents one single error class Error CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict] - MESSAGE_OPTIONS = [:message] + MESSAGE_OPTIONS = [:message, :full_message, :message_format] class_attribute :i18n_customize_full_message, default: false - def self.full_message(attribute, message, base) # :nodoc: + def self.full_message(attribute, message, base, message_format = nil) # :nodoc: return message if attribute == :base base_class = base.class @@ -46,8 +46,14 @@ def self.full_message(attribute, message, base) # :nodoc: defaults = [] end - defaults << :"errors.format" - defaults << "%{attribute} %{message}" + + if message_format + defaults << :"errors._hardcoded_format" + defaults << message_format + else + defaults << :"errors.format" + defaults << "%{attribute} %{message}" + end attr_name = attribute.tr(".", "_").humanize attr_name = base_class.human_attribute_name(attribute, { @@ -106,6 +112,11 @@ def initialize(base, attribute, type = :invalid, **options) @raw_type = type @type = type || :invalid @options = options + + if full_message = @options.delete(:full_message) + @options[:message] = full_message + @options[:message_format] ||= "%{message}" + end end def initialize_dup(other) # :nodoc: @@ -156,7 +167,7 @@ def details # error.full_message # # => "Name is too short (minimum is 5 characters)" def full_message - self.class.full_message(attribute, message, @base) + self.class.full_message(attribute, message, @base, @options[:message_format]) end # See if error matches provided +attribute+, +type+ and +options+. diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index e4e4b6cf1d58e..b2a305dbad068 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -518,8 +518,8 @@ def messages_for(attribute) # Returns a full message for a given attribute. # # person.errors.full_message(:name, 'is invalid') # => "Name is invalid" - def full_message(attribute, message) - Error.full_message(attribute, message, @base) + def full_message(attribute, message, message_format: false) + Error.full_message(attribute, message, @base, message_format) end # Translates an error message in its default scope diff --git a/activemodel/test/cases/error_test.rb b/activemodel/test/cases/error_test.rb index 88755f97d201d..f2be9f0c2fe1a 100644 --- a/activemodel/test/cases/error_test.rb +++ b/activemodel/test/cases/error_test.rb @@ -176,6 +176,28 @@ def test_initialize assert_equal "press the button", error.full_message end + test "full_message returns the given message when passed full_message option" do + error = ActiveModel::Error.new(Person.new, :name, full_message: "press the button") + assert_equal "press the button", error.full_message + end + + test "full_message returns the given message when passed message_format option" do + error = ActiveModel::Error.new(Person.new, :name, message: "press the button", message_format: "%{message}") + assert_equal "press the button", error.full_message + + error = ActiveModel::Error.new(Person.new, :name, message: "should be valid", message_format: "%{attribute} %{message}") + assert_equal "name should be valid", error.full_message + + error = ActiveModel::Error.new(Person.new, :name, :blank, message_format: "%{attribute} %{message}") + assert_equal "name can't be blank", error.full_message + + error = ActiveModel::Error.new(Person.new, :name, :blank, message_format: "%{message}") + assert_equal "can't be blank", error.full_message + + error = ActiveModel::Error.new(Person.new, :name, :blank, message_format: "something hardcoded") + assert_equal "something hardcoded", error.full_message + end + test "full_message returns the given message with the attribute name included" do error = ActiveModel::Error.new(Person.new, :name, :blank) assert_equal "name can't be blank", error.full_message @@ -232,7 +254,9 @@ def test_initialize allow_nil: false, allow_blank: false, strict: true, - message: "message" + message: "message", + message_format: "%{message}", + full_message: "message", ) assert_equal( diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index b9f564fb4d0f5..101196ec3f8d4 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -596,6 +596,12 @@ def call assert_equal "press the button", person.errors.full_message(:base, "press the button") end + test "full_message returns the given message when message_format is passed in" do + person = Person.new + assert_equal "cannot be blank", person.errors.full_message(:name, "cannot be blank", message_format: "%{message}") + assert_equal "cannot be blank", person.errors.full_message(:name_test, "cannot be blank", message_format: "%{message}") + end + test "full_message returns the given message with the attribute name included" do person = Person.new assert_equal "name cannot be blank", person.errors.full_message(:name, "cannot be blank") From 47f8f749ab45912221fd793dd3630d294914d5ae Mon Sep 17 00:00:00 2001 From: Alexandre Barret Date: Fri, 1 Oct 2021 22:00:16 +1300 Subject: [PATCH 2/5] Switch to full_message_format option --- activemodel/lib/active_model/error.rb | 30 +++++++++++--------------- activemodel/lib/active_model/errors.rb | 4 ++-- activemodel/test/cases/error_test.rb | 21 ++++++++++++------ activemodel/test/cases/errors_test.rb | 6 +++--- 4 files changed, 31 insertions(+), 30 deletions(-) diff --git a/activemodel/lib/active_model/error.rb b/activemodel/lib/active_model/error.rb index 6ef9e343851c6..2b6fbc9f79c5c 100644 --- a/activemodel/lib/active_model/error.rb +++ b/activemodel/lib/active_model/error.rb @@ -8,17 +8,19 @@ module ActiveModel # Represents one single error class Error CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict] - MESSAGE_OPTIONS = [:message, :full_message, :message_format] + MESSAGE_OPTIONS = [:message, :full_message, :full_message_format] class_attribute :i18n_customize_full_message, default: false - def self.full_message(attribute, message, base, message_format = nil) # :nodoc: + def self.full_message(attribute, message, base, full_message_format = nil) # :nodoc: return message if attribute == :base base_class = base.class attribute = attribute.to_s - if i18n_customize_full_message && base_class.respond_to?(:i18n_scope) + defaults = if full_message_format + [:"errors._hardcoded_format", full_message_format] + elsif i18n_customize_full_message && base_class.respond_to?(:i18n_scope) attribute = attribute.remove(/\[\d+\]/) parts = attribute.split(".") attribute_name = parts.pop @@ -26,34 +28,26 @@ def self.full_message(attribute, message, base, message_format = nil) # :nodoc: attributes_scope = "#{base_class.i18n_scope}.errors.models" if namespace - defaults = base_class.lookup_ancestors.map do |klass| + base_class.lookup_ancestors.flat_map do |klass| [ :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.attributes.#{attribute_name}.format", :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.format", ] end else - defaults = base_class.lookup_ancestors.map do |klass| + base_class.lookup_ancestors.flat_map do |klass| [ :"#{attributes_scope}.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.format", :"#{attributes_scope}.#{klass.model_name.i18n_key}.format", ] end end - - defaults.flatten! else - defaults = [] + [] end - - if message_format - defaults << :"errors._hardcoded_format" - defaults << message_format - else - defaults << :"errors.format" - defaults << "%{attribute} %{message}" - end + defaults << :"errors.format" + defaults << "%{attribute} %{message}" attr_name = attribute.tr(".", "_").humanize attr_name = base_class.human_attribute_name(attribute, { @@ -115,7 +109,7 @@ def initialize(base, attribute, type = :invalid, **options) if full_message = @options.delete(:full_message) @options[:message] = full_message - @options[:message_format] ||= "%{message}" + @options[:full_message_format] ||= "%{message}" end end @@ -167,7 +161,7 @@ def details # error.full_message # # => "Name is too short (minimum is 5 characters)" def full_message - self.class.full_message(attribute, message, @base, @options[:message_format]) + self.class.full_message(attribute, message, @base, @options[:full_message_format]) end # See if error matches provided +attribute+, +type+ and +options+. diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index b2a305dbad068..4cc71d5e30e74 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -518,8 +518,8 @@ def messages_for(attribute) # Returns a full message for a given attribute. # # person.errors.full_message(:name, 'is invalid') # => "Name is invalid" - def full_message(attribute, message, message_format: false) - Error.full_message(attribute, message, @base, message_format) + def full_message(attribute, message, full_message_format: false) + Error.full_message(attribute, message, @base, full_message_format) end # Translates an error message in its default scope diff --git a/activemodel/test/cases/error_test.rb b/activemodel/test/cases/error_test.rb index f2be9f0c2fe1a..edc38f5de56b4 100644 --- a/activemodel/test/cases/error_test.rb +++ b/activemodel/test/cases/error_test.rb @@ -181,21 +181,28 @@ def test_initialize assert_equal "press the button", error.full_message end - test "full_message returns the given message when passed message_format option" do - error = ActiveModel::Error.new(Person.new, :name, message: "press the button", message_format: "%{message}") + test "full_message returns the given message when passed full_message_format option" do + error = ActiveModel::Error.new(Person.new, :name, message: "press the button", full_message_format: "%{message}") assert_equal "press the button", error.full_message - error = ActiveModel::Error.new(Person.new, :name, message: "should be valid", message_format: "%{attribute} %{message}") + error = ActiveModel::Error.new(Person.new, :name, message: "should be valid", full_message_format: "%{attribute} %{message}") assert_equal "name should be valid", error.full_message - error = ActiveModel::Error.new(Person.new, :name, :blank, message_format: "%{attribute} %{message}") + error = ActiveModel::Error.new(Person.new, :name, :blank, full_message_format: "%{attribute} %{message}") assert_equal "name can't be blank", error.full_message - error = ActiveModel::Error.new(Person.new, :name, :blank, message_format: "%{message}") + error = ActiveModel::Error.new(Person.new, :name, :blank, full_message_format: "%{message}") assert_equal "can't be blank", error.full_message - error = ActiveModel::Error.new(Person.new, :name, :blank, message_format: "something hardcoded") + error = ActiveModel::Error.new(Person.new, :name, :blank, full_message_format: "something hardcoded") assert_equal "something hardcoded", error.full_message + + # Use a locale without errors.format + error = ActiveModel::Error.new(Person.new, :name, full_message: "can't be blank") + I18n.with_locale(:unknown) { assert_equal "can't be blank", error.full_message } + + error = ActiveModel::Error.new(Person.new, :name, message: "can't be blank", full_message_format: "%{message}") + I18n.with_locale(:unknown) { assert_equal "can't be blank", error.full_message } end test "full_message returns the given message with the attribute name included" do @@ -255,7 +262,7 @@ def test_initialize allow_blank: false, strict: true, message: "message", - message_format: "%{message}", + full_message_format: "%{message}", full_message: "message", ) diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 101196ec3f8d4..31a3ff1fa70b3 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -596,10 +596,10 @@ def call assert_equal "press the button", person.errors.full_message(:base, "press the button") end - test "full_message returns the given message when message_format is passed in" do + test "full_message returns the given message when full_message_format is passed in" do person = Person.new - assert_equal "cannot be blank", person.errors.full_message(:name, "cannot be blank", message_format: "%{message}") - assert_equal "cannot be blank", person.errors.full_message(:name_test, "cannot be blank", message_format: "%{message}") + assert_equal "cannot be blank", person.errors.full_message(:name, "cannot be blank", full_message_format: "%{message}") + assert_equal "cannot be blank", person.errors.full_message(:name_test, "cannot be blank", full_message_format: "%{message}") end test "full_message returns the given message with the attribute name included" do From 154178e8813e413c41384e457b6d115724146691 Mon Sep 17 00:00:00 2001 From: Alexandre Barret Date: Fri, 1 Oct 2021 17:15:09 +1300 Subject: [PATCH 3/5] Add API doc for :full_message_format and :full_message options --- activemodel/lib/active_model/validations/absence.rb | 2 ++ activemodel/lib/active_model/validations/acceptance.rb | 2 ++ activemodel/lib/active_model/validations/confirmation.rb | 2 ++ activemodel/lib/active_model/validations/exclusion.rb | 2 ++ activemodel/lib/active_model/validations/format.rb | 2 ++ activemodel/lib/active_model/validations/inclusion.rb | 2 ++ activemodel/lib/active_model/validations/length.rb | 2 ++ activemodel/lib/active_model/validations/numericality.rb | 2 ++ activemodel/lib/active_model/validations/presence.rb | 2 ++ activemodel/lib/active_model/validations/validates.rb | 4 ++-- 10 files changed, 20 insertions(+), 2 deletions(-) diff --git a/activemodel/lib/active_model/validations/absence.rb b/activemodel/lib/active_model/validations/absence.rb index 3d26c0e661bbf..5c0a9b45db54b 100644 --- a/activemodel/lib/active_model/validations/absence.rb +++ b/activemodel/lib/active_model/validations/absence.rb @@ -21,6 +21,8 @@ module HelperMethods # # Configuration options: # * :message - A custom error message (default is: "must be blank"). + # * :full_message_format - Format of full_message (default is: "%{attribute} %{message}"). + # * :full_message - A custom error message with "%{message}" as a full_message_format # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 5ea03cb124ffb..e0ea188d31c6c 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -95,6 +95,8 @@ module HelperMethods # Configuration options: # * :message - A custom error message (default is: "must be # accepted"). + # * :full_message_format - Format of full_message (default is: "%{attribute} %{message}"). + # * :full_message - A custom error message with "%{message}" as a full_message_format # * :accept - Specifies a value that is considered accepted. # Also accepts an array of possible values. The default value is # an array ["1", true], which makes it easy to relate to an HTML diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index d9c560acf4107..d7d05f60e0a2f 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -66,6 +66,8 @@ module HelperMethods # Configuration options: # * :message - A custom error message (default is: "doesn't match # %{translated_attribute_name}"). + # * :full_message_format - Format of full_message (default is: "%{attribute} %{message}"). + # * :full_message - A custom error message with "%{message}" as a full_message_format # * :case_sensitive - Looks for an exact match. Ignored by # non-text columns (+true+ by default). # diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index a2e4dd865237d..c6c3ce8f4235a 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -37,6 +37,8 @@ module HelperMethods # Range#cover?, otherwise with include?. # * :message - Specifies a custom error message (default is: "is # reserved"). + # * :full_message_format - Format of full_message (default is: "%{attribute} %{message}"). + # * :full_message - A custom error message with "%{message}" as a full_message_format # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index 92493a265896c..3f49ab258713d 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -91,6 +91,8 @@ module HelperMethods # # Configuration options: # * :message - A custom error message (default is: "is invalid"). + # * :full_message_format - Format of full_message (default is: "%{attribute} %{message}"). + # * :full_message - A custom error message with "%{message}" as a full_message_format # * :with - Regular expression that if the attribute matches will # result in a successful validation. This can be provided as a proc or # lambda returning regular expression which will be called at runtime. diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index bbd99676c3595..4cce969d3ffd1 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -35,6 +35,8 @@ module HelperMethods # * :within - A synonym(or alias) for :in # * :message - Specifies a custom error message (default is: "is # not included in the list"). + # * :full_message_format - Format of full_message (default is: "%{attribute} %{message}"). + # * :full_message - A custom error message with "%{message}" as a full_message_format # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 57db6c68166ee..c455cf19f12bc 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -115,6 +115,8 @@ module HelperMethods # * :message - The error message to use for a :minimum, # :maximum, or :is violation. An alias of the appropriate # too_long/too_short/wrong_length message. + # * :full_message_format - Format of full_message (default is: "%{attribute} %{message}"). + # * :full_message - A custom error message with "%{message}" as a full_message_format # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+ and +:strict+. diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index c7871c4ed8117..742be10af2fbd 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -160,6 +160,8 @@ module HelperMethods # # Configuration options: # * :message - A custom error message (default is: "is not a number"). + # * :full_message_format - Format of full_message (default is: "%{attribute} %{message}"). + # * :full_message - A custom error message with "%{message}" as a full_message_format # * :only_integer - Specifies whether the value has to be an # integer (default is +false+). # * :allow_nil - Skip validation if attribute is +nil+ (default is diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 9ac376c5280ca..723c4642f118e 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -27,6 +27,8 @@ module HelperMethods # # Configuration options: # * :message - A custom error message (default is: "can't be blank"). + # * :full_message_format - Format of full_message (default is: "%{attribute} %{message}"). + # * :full_message - A custom error message with "%{message}" as a full_message_format # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 20a34c56ccfe3..a153dc2b30b1d 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -99,8 +99,8 @@ module ClassMethods # validates :token, length: 24, strict: TokenLengthException # # - # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+, +:strict+ - # and +:message+ can be given to one specific validator, as a hash: + # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+, +:strict+, + # +:message+, +:full_message_format+ and +:full_message+ can be given to one specific validator, as a hash: # # validates :password, presence: { if: :password_required?, message: 'is forgotten.' }, confirmation: true def validates(*attributes) From 68861b7f10bafc2497cd16302366c769d05a7517 Mon Sep 17 00:00:00 2001 From: Alexandre Barret Date: Fri, 1 Oct 2021 17:48:27 +1300 Subject: [PATCH 4/5] Update ActiveRecord validations documentation The documentation now provides descriptions for both :full_message and full_message_format options. --- guides/source/active_record_validations.md | 96 ++++++++++++++++++++-- 1 file changed, 89 insertions(+), 7 deletions(-) diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index de27dfd2e02dd..58f633d4b7bd7 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -272,13 +272,16 @@ validated. Each helper accepts an arbitrary number of attribute names, so with a single line of code you can add the same kind of validation to several attributes. -All of them accept the `:on` and `:message` options, which define when the -validation should be run and what message should be added to the `errors` -collection if it fails, respectively. The `:on` option takes one of the values -`:create` or `:update`. There is a default error -message for each one of the validation helpers. These messages are used when -the `:message` option isn't specified. Let's take a look at each one of the -available helpers. +All of them accept the `:on`, `:message` and `:full_message_format` options, +which define when the validation should be run, what message should be added +to the `errors` collection if it fails and how this message should be formatted, +respectively. + +The `:on` option takes one of the values `:create` or `:update`. There is a +default error message for each one of the validation helpers. These messages +are used when the `:message` option isn't specified. `"%{attribute} %{message}"` +is the default for all validation helpers `:full_message_format` option. +Let's take a look at each one of the available helpers. ### `acceptance` @@ -303,6 +306,28 @@ class Person < ApplicationRecord end ``` +You can pass a message format via the `full_message_format` option. +You can use both `attribute` and `message` to format your full error message. +This format will take precedence over a format defined in a locale file using +I18n. + +```ruby +class Person < ApplicationRecord + validates :terms_of_service, acceptance: { + full_message_format: '** %{attribute} / %{message} **' + } +end +``` + +Finally, you can pass format a custom message via the `full_message` option. +This is the same as passing a custom message with the `"%{message}"` full message format + +```ruby +class Person < ApplicationRecord + validates :terms_of_service, acceptance: { full_message: 'Terms must be agreed' } +end +``` + It can also receive an `:accept` option, which determines the allowed values that will be considered as accepted. It defaults to `['1', true]` and can be easily changed. @@ -877,6 +902,63 @@ class Person < ApplicationRecord end ``` +### `:full_message_format` + +This option allows you to format your full error message when calling +`Error#full_message`. It can optionally contain any/all of `%{attribute}` and +`%{message}` which will be dynamically replaced when validation fails. This +replacement is done using the I18n gem, and the placeholders must match +exactly, no spaces are allowed. + +```ruby +class Person < ApplicationRecord + validates :name, presence: { full_message_format: "** %{attribute} /**/ %{message} **" } +end +``` + +```irb +person = Person.new(name: nil) +person.valid? + +puts person.errors.full_messages +=> ['** name /**/ must be present **'] + +person.errors.where(:name).map(&:message) +=> ["must be present"] +``` + +### `:full_message` + +This option is a helper passing both a custom `message` with `%{message}` as a `full_message_format` + +```ruby +# These definitions are equivalent + +class Person < ApplicationRecord + validates :name, presence: { + full_message: 'Person requires a name' + } +end + +class Person < ApplicationRecord + validates :name, presence: { + message: 'Person requires a name', + full_message_format: "%{message}" + } +end +``` + +```irb +person = Person.new(name: nil) +person.valid? + +puts person.errors.full_messages +=> ['Person requires a name'] + +person.errors.where(:name).map(&:message) +=> ['Person requires a name'] +``` + ### `:on` The `:on` option lets you specify when the validation should happen. The From cb377301cd4cb2d5c1993d35b4ef9c20a8d824b2 Mon Sep 17 00:00:00 2001 From: Alexandre Barret Date: Fri, 1 Oct 2021 23:13:03 +1300 Subject: [PATCH 5/5] Add format full validation messages section on ActiveRecord validations guides --- guides/source/active_record_validations.md | 97 ++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index 58f633d4b7bd7..306d9c1e60cf2 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -1454,6 +1454,103 @@ irb> person.errors.size => 0 ``` +Formatting Full Validation Messages +----------------------------------- + +There are multiple ways to format your error messages. + +### i18n + +ActiveRecord and ActiveModels both use i18n as their main way to format a +full validation message. When using i18n you can format your full messages by +adding a format key to your locale file. + +To enable the formatting of full messages, the `i18n_customize_full_message` +config must be set to true in your `config/application.rb` file. + +For example, if we have a class and locale defined as such + +```ruby +class Person < ApplicationRecord + validates :name, presence: true + validates :age, numericality: true +end +``` + +```yml +activerecord: + errors: + models: + person: + attributes: + format: "** %{attribute} %{message} **" + name: + format: "-- %{message} --" + blank: "can't be blank" + age: + not_a_number: "is not a number" +``` + +You will get these results + +```irb +person = Person.new.tap(&:valid?) + +person.errors.full_messages +["-- can't be blank --", "** Age is not a number **"] + +person.errors.where(:age).map(&:message) +["is not a number"] + +person.errors.where(:name).map(&:message) +["can't be blank"] +``` + +### :full_message_format & :full_message options + +When not using i18n you can pass the `:full_message_format` option on a +validation helper to format your full messages. This definition will take +precedence over i18n. You can optionally use `%{attribute}` and `%{message}` +which will be dynamically replaced when validation fails. + +`:full_message` option is a shorter version for a custom message with +`"%{message}"` as `full_message_format` + +```ruby +class Person + include ActiveModel::Model + attr_accessor :age, :name + + validates :name, presence: { full_message: 'A person name cannot be blank' } + validates :age, numericality: { + message: 'The age must be a number', + full_message_format: '%{message}' + } +end +``` + +```irb +person = Person.new.tap(&:valid?) + +person.errors.full_messages +["The age must be a number", "A person name cannot be blank"] + +person.errors.where(:name).map(&:message) +["A person name cannot be blank"] + +# works with Errors#add + +person = Person.new +person.errors.add(:name, full_message: 'A person name cannot be blank') +person.errors.add(:age, 'The age must be a number', full_message_format: '%{message}') + +person.errors.full_messages +["The age must be a number", "A person name cannot be blank"] + +person.errors.where(:name).map(&:message) +["A person name cannot be blank"] +``` + Displaying Validation Errors in Views -------------------------------------