Skip to content

Commit

Permalink
activemodel: ActiveModel::Errors#add accepts String and Proc for type (
Browse files Browse the repository at this point in the history
…#717)

* refactor(activemodel): move ActiveModel::Errors#add from generated

* fix(activemodel): ActiveModel::Errors#add accepts String and Proc for type

The behavior of passing String or Proc for type should be allowed.

FYI: It is documented in 6.1, but It is already available in 6.0.

cf. rails/rails@fcd1e41#diff-4deafad6fefcf6aa6626a1f7e3cd0ca1cf95e358f55a10a69e02be84d926bb31R368-R374

cf. rails/rails@d9011e3#diff-4deafad6fefcf6aa6626a1f7e3cd0ca1cf95e358f55a10a69e02be84d926bb31R370

Also, the name of the second argument was changed from `message` to `type` in 6.1.

* fix(activemodel): Divide type definition of ActiveModel::Errors#add to 6.0 and 7.0

ActiveModel::Error was added on 6.1.

cf. rails/rails@ef68d3e

On 6.1 or later, ActiveModel::Errors#add returns ActiveModel::Error.

On the other hand, `ActiveModel::Errors#add` returns messages array on rails 6.0, because last evaluated is `Array#<<`.

cf. https://github.com/rails/rails/blob/v6.0.0/activemodel/lib/active_model/errors.rb#L311-L322

Currently in gem_rbs_collection, activemodel type definitions are defined 6.0 and 7.0, but not 6.1.

cf. #615

So I divide type definition of ActiveModel::Errors#add to 6.0 and 7.0.
  • Loading branch information
sanfrecce-osaka authored Dec 31, 2024
1 parent ccbd2bb commit 09beb21
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 47 deletions.
9 changes: 9 additions & 0 deletions gems/activemodel/6.0/_test/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@ class Person

validates :name, presence: true, length: { maximum: 100 }
validates :email, presence: true, if: [:foo?, -> { age >= 20 }]
validate :should_be_satisfied_special_email_rule

def foo? = true

def should_be_satisfied_special_email_rule
if Time.current >= Time.zone.local(2024, 10)
errors.add(:email, -> (_person, _options) { "must be satisfied at least 3 rules after #{Time.zone.local(2024, 10)}" }) if [/a-z/, /A-Z/, /0-9/, /[+]/].count {|rule| email.match?(rule) } > 3
else
errors.add(:email, 'must be satisfied at least 2 rules') if [/a-z/, /A-Z/, /0-9/].count {|rule| email.match?(rule) } > 2
end
end
end

person = Person.new(name: 'John Doe')
Expand Down
1 change: 1 addition & 0 deletions gems/activemodel/6.0/_test/test.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ class Person
attr_accessor email: String

def foo?: () -> bool
def should_be_satisfied_special_email_rule: () -> void
end
43 changes: 43 additions & 0 deletions gems/activemodel/6.0/activemodel-6.0.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,48 @@ module ActiveModel
# person.errors.delete(:name) # => ["cannot be nil"]
# person.errors[:name] # => []
def delete: (untyped key) -> untyped

# Adds +message+ to the error messages and used validator type to +details+ on +attribute+.
# More than one error can be added to the same +attribute+.
# If no +message+ is supplied, <tt>:invalid</tt> is assumed.
#
# person.errors.add(:name)
# # => ["is invalid"]
# person.errors.add(:name, :not_implemented, message: "must be implemented")
# # => ["is invalid", "must be implemented"]
#
# person.errors.messages
# # => {:name=>["is invalid", "must be implemented"]}
#
# person.errors.details
# # => {:name=>[{error: :not_implemented}, {error: :invalid}]}
#
# If +message+ is a symbol, it will be translated using the appropriate
# scope (see +generate_message+).
#
# If +message+ is a proc, it will be called, allowing for things like
# <tt>Time.now</tt> to be used within an error.
#
# If the <tt>:strict</tt> option is set to +true+, it will raise
# ActiveModel::StrictValidationFailed instead of adding the error.
# <tt>:strict</tt> option can also be set to any other exception.
#
# person.errors.add(:name, :invalid, strict: true)
# # => ActiveModel::StrictValidationFailed: Name is invalid
# person.errors.add(:name, :invalid, strict: NameIsInvalid)
# # => NameIsInvalid: Name is invalid
#
# person.errors.messages # => {}
#
# +attribute+ should be set to <tt>:base</tt> if the error is not
# directly associated with a single attribute.
#
# person.errors.add(:base, :name_or_email_blank,
# message: "either name or email must be present")
# person.errors.messages
# # => {:base=>["either name or email must be present"]}
# person.errors.details
# # => {:base=>[{error: :name_or_email_blank}]}
def add: (untyped attribute, ?::Symbol | ::String | ^(untyped base, ::Hash[untyped, untyped]) -> (::Symbol | ::String) type, ?::Hash[untyped, untyped] options) -> Array[String]
end
end
43 changes: 0 additions & 43 deletions gems/activemodel/6.0/activemodel-generated.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -1493,49 +1493,6 @@ module ActiveModel
# person.errors.to_hash(true) # => {:name=>["name cannot be nil"]}
def to_hash: (?bool full_messages) -> untyped

# Adds +message+ to the error messages and used validator type to +details+ on +attribute+.
# More than one error can be added to the same +attribute+.
# If no +message+ is supplied, <tt>:invalid</tt> is assumed.
#
# person.errors.add(:name)
# # => ["is invalid"]
# person.errors.add(:name, :not_implemented, message: "must be implemented")
# # => ["is invalid", "must be implemented"]
#
# person.errors.messages
# # => {:name=>["is invalid", "must be implemented"]}
#
# person.errors.details
# # => {:name=>[{error: :not_implemented}, {error: :invalid}]}
#
# If +message+ is a symbol, it will be translated using the appropriate
# scope (see +generate_message+).
#
# If +message+ is a proc, it will be called, allowing for things like
# <tt>Time.now</tt> to be used within an error.
#
# If the <tt>:strict</tt> option is set to +true+, it will raise
# ActiveModel::StrictValidationFailed instead of adding the error.
# <tt>:strict</tt> option can also be set to any other exception.
#
# person.errors.add(:name, :invalid, strict: true)
# # => ActiveModel::StrictValidationFailed: Name is invalid
# person.errors.add(:name, :invalid, strict: NameIsInvalid)
# # => NameIsInvalid: Name is invalid
#
# person.errors.messages # => {}
#
# +attribute+ should be set to <tt>:base</tt> if the error is not
# directly associated with a single attribute.
#
# person.errors.add(:base, :name_or_email_blank,
# message: "either name or email must be present")
# person.errors.messages
# # => {:base=>["either name or email must be present"]}
# person.errors.details
# # => {:base=>[{error: :name_or_email_blank}]}
def add: (untyped attribute, ?::Symbol message, ?::Hash[untyped, untyped] options) -> untyped

# Returns +true+ if an error on the attribute with the given message is
# present, or +false+ otherwise. +message+ is treated the same as for +add+.
#
Expand Down
2 changes: 1 addition & 1 deletion gems/activemodel/6.0/activemodel.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ module ActiveModel
# person = Person.new
# person.valid? # => false
# person.errors # => #<ActiveModel::Errors:0x007fe603816640 @messages={name:["can't be blank"]}>
def errors: () -> untyped
def errors: () -> Errors

# Runs all the specified validations and returns +true+ if no errors were
# added otherwise +false+.
Expand Down
22 changes: 19 additions & 3 deletions gems/activemodel/7.0/_test/test.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
require "active_model"

class Person
include ActiveModel::Model
include ActiveModel::Validations

# @dynamic name, name=
attr_accessor :name
# @dynamic name, name=, email, email=
attr_accessor :name, :email

validate :should_be_satisfied_special_email_rule

def should_be_satisfied_special_email_rule
return unless email

if Time.current >= Time.zone.local(2024, 10)
errors.add(:email, -> (_person, _options) { "must be satisfied at least 3 rules after #{Time.zone.local(2024, 10)}" }) if [/a-z/, /A-Z/, /0-9/, /[+]/].count {|rule| email.match?(rule) } > 3
else
errors.add(:email, 'must be satisfied at least 2 rules') if [/a-z/, /A-Z/, /0-9/].count {|rule| email.match?(rule) } > 2
end
end
end

ActiveModel::Error::CALLBACKS_OPTIONS
ActiveModel::Error::MESSAGE_OPTIONS

error = ActiveModel::Error.new(Person.new, :name, :too_short, count: 5)
person = Person.new(name: 'John Doe')
person.valid?

error = ActiveModel::Error.new(person, :name, :too_short, count: 10)
error.attribute
error.type
error.options
Expand Down
4 changes: 4 additions & 0 deletions gems/activemodel/7.0/_test/test.rbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
class Person
include ActiveModel::Model
include ActiveModel::Validations
extend ActiveModel::Validations::ClassMethods

attr_accessor name: String?
attr_accessor email: String?

def should_be_satisfied_special_email_rule: () -> void
end
47 changes: 47 additions & 0 deletions gems/activemodel/7.0/activemodel-7.0.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,53 @@ module ActiveModel
end
end

module ActiveModel
class Errors
# Adds +message+ to the error messages and used validator type to +details+ on +attribute+.
# More than one error can be added to the same +attribute+.
# If no +message+ is supplied, <tt>:invalid</tt> is assumed.
#
# person.errors.add(:name)
# # => ["is invalid"]
# person.errors.add(:name, :not_implemented, message: "must be implemented")
# # => ["is invalid", "must be implemented"]
#
# person.errors.messages
# # => {:name=>["is invalid", "must be implemented"]}
#
# person.errors.details
# # => {:name=>[{error: :not_implemented}, {error: :invalid}]}
#
# If +message+ is a symbol, it will be translated using the appropriate
# scope (see +generate_message+).
#
# If +message+ is a proc, it will be called, allowing for things like
# <tt>Time.now</tt> to be used within an error.
#
# If the <tt>:strict</tt> option is set to +true+, it will raise
# ActiveModel::StrictValidationFailed instead of adding the error.
# <tt>:strict</tt> option can also be set to any other exception.
#
# person.errors.add(:name, :invalid, strict: true)
# # => ActiveModel::StrictValidationFailed: Name is invalid
# person.errors.add(:name, :invalid, strict: NameIsInvalid)
# # => NameIsInvalid: Name is invalid
#
# person.errors.messages # => {}
#
# +attribute+ should be set to <tt>:base</tt> if the error is not
# directly associated with a single attribute.
#
# person.errors.add(:base, :name_or_email_blank,
# message: "either name or email must be present")
# person.errors.messages
# # => {:base=>["either name or email must be present"]}
# person.errors.details
# # => {:base=>[{error: :name_or_email_blank}]}
def add: (untyped attribute, ?::Symbol | ::String | ^(untyped base, ::Hash[untyped, untyped]) -> (::Symbol | ::String) type, ?::Hash[untyped, untyped] options) -> Error
end
end

module ActiveModel
class Error
CALLBACKS_OPTIONS: ::Array[:if | :unless | :on | :allow_nil | :allow_blank | :strict]
Expand Down

0 comments on commit 09beb21

Please sign in to comment.