rails/activemodel/CHANGELOG.md

127 lines
3.7 KiB
Markdown
Raw Normal View History

* Raise `NoMethodError` in `ActiveModel::Type::Value#as_json` to avoid unpredictable
results.
*Vasiliy Ermolovich*
Avoid double cast in types that only override cast Follow-up to #44625. In #44625, the `SerializeCastValue` module was added to allow types to avoid a redundant call to `cast` when serializing a value for the database. Because it introduced a new method (`serialize_cast_value`) that was not part of the `ActiveModel::Type::Value` contract, it was designed to be opt-in. Furthermore, to guard against incompatible `serialize` and `serialize_cast_value` implementations in types that override `serialize` but (unintentionally) inherit `serialize_cast_value`, types were required to explicitly include the `SerializeCastValue` module to activate the optimization. i.e. It was not sufficient just to have `SerializeCastValue` in the ancestor chain. The `SerializeCastValue` module is not part of the public API, and there are no plans to change that, which meant user-created custom types could not benefit from this optimization. This commit changes the opt-in condition such that it is sufficient for the owner of the `serialize_cast_value` method to be the same or below the owner of the `serialize` method in the ancestor chain. This means a user-created type that only overrides `cast`, **not** `serialize`, will now benefit from the optimization. For example, a type like: ```ruby class DowncasedString < ActiveModel::Type::String def cast(value) super&.downcase end end ``` As demonstrated in the benchmark below, this commit does not change the current performance of the built-in Active Model types. However, for a simple custom type like `DowncasedString`, the performance of `value_for_database` is twice as fast. For types with more expensive `cast` operations, the improvement may be greater. **Benchmark** ```ruby # frozen_string_literal: true require "benchmark/ips" require "active_model" class DowncasedString < ActiveModel::Type::String def cast(value) super&.downcase end end ActiveModel::Type.register(:downcased_string, DowncasedString) VALUES = { my_big_integer: "123456", my_boolean: "true", my_date: "1999-12-31", my_datetime: "1999-12-31 12:34:56 UTC", my_decimal: "123.456", my_float: "123.456", my_immutable_string: "abcdef", my_integer: "123456", my_string: "abcdef", my_time: "1999-12-31T12:34:56.789-10:00", my_downcased_string: "AbcDef", } TYPES = VALUES.to_h { |name, value| [name, name.to_s.delete_prefix("my_").to_sym] } class MyModel include ActiveModel::API include ActiveModel::Attributes TYPES.each do |name, type| attribute name, type end end attribute_set = MyModel.new(VALUES).instance_variable_get(:@attributes) TYPES.each do |name, type| attribute = attribute_set[name.to_s] Benchmark.ips do |x| x.report(type.to_s) { attribute.value_for_database } end end ``` **Before** ``` big_integer 2.986M (± 1.2%) i/s - 15.161M in 5.078972s boolean 2.980M (± 1.1%) i/s - 15.074M in 5.059456s date 2.960M (± 1.1%) i/s - 14.831M in 5.011355s datetime 1.368M (± 0.9%) i/s - 6.964M in 5.092074s decimal 2.930M (± 1.2%) i/s - 14.911M in 5.089048s float 2.932M (± 1.3%) i/s - 14.713M in 5.018512s immutable_string 3.013M (± 1.3%) i/s - 15.239M in 5.058085s integer 1.603M (± 0.8%) i/s - 8.096M in 5.052046s string 2.977M (± 1.1%) i/s - 15.168M in 5.094874s time 1.338M (± 0.9%) i/s - 6.699M in 5.006046s downcased_string 1.394M (± 0.9%) i/s - 7.034M in 5.046972s ``` **After** ``` big_integer 3.016M (± 1.0%) i/s - 15.238M in 5.053005s boolean 2.965M (± 1.3%) i/s - 15.037M in 5.071921s date 2.924M (± 1.0%) i/s - 14.754M in 5.046294s datetime 1.435M (± 0.9%) i/s - 7.295M in 5.082498s decimal 2.950M (± 0.9%) i/s - 14.800M in 5.017225s float 2.964M (± 0.9%) i/s - 14.987M in 5.056405s immutable_string 2.907M (± 1.4%) i/s - 14.677M in 5.049194s integer 1.638M (± 0.9%) i/s - 8.227M in 5.022401s string 2.971M (± 1.0%) i/s - 14.891M in 5.011709s time 1.454M (± 0.9%) i/s - 7.384M in 5.079993s downcased_string 2.939M (± 0.9%) i/s - 14.872M in 5.061100s ```
2022-10-09 04:34:17 +08:00
* Custom attribute types that inherit from Active Model built-in types and do
not override the `serialize` method will now benefit from an optimization
when serializing attribute values for the database.
For example, with a custom type like the following:
```ruby
class DowncasedString < ActiveModel::Type::String
def cast(value)
super&.downcase
end
end
ActiveRecord::Type.register(:downcased_string, DowncasedString)
class User < ActiveRecord::Base
attribute :email, :downcased_string
end
user = User.new(email: "FooBar@example.com")
```
Serializing the `email` attribute for the database will be roughly twice as
fast. More expensive `cast` operations will likely see greater improvements.
*Jonathan Hefner*
* `has_secure_password` now supports password challenges via a
`password_challenge` accessor and validation.
A password challenge is a safeguard to verify that the current user is
actually the password owner. It can be used when changing sensitive model
fields, such as the password itself. It is different than a password
confirmation, which is used to prevent password typos.
When `password_challenge` is set, the validation checks that the value's
digest matches the *currently persisted* `password_digest` (i.e.
`password_digest_was`).
This allows a password challenge to be done as part of a typical `update`
call, just like a password confirmation. It also allows a password
challenge error to be handled in the same way as other validation errors.
For example, in the controller, instead of:
```ruby
password_params = params.require(:password).permit(
:password_challenge,
:password,
:password_confirmation,
)
password_challenge = password_params.delete(:password_challenge)
@password_challenge_failed = !current_user.authenticate(password_challenge)
if !@password_challenge_failed && current_user.update(password_params)
# ...
end
```
You can now write:
```ruby
password_params = params.require(:password).permit(
:password_challenge,
:password,
:password_confirmation,
).with_defaults(password_challenge: "")
if current_user.update(password_params)
# ...
end
```
And, in the view, instead of checking `@password_challenge_failed`, you can
render an error for the `password_challenge` field just as you would for
other form fields, including utilizing `config.action_view.field_error_proc`.
*Jonathan Hefner*
* Support infinite ranges for `LengthValidator`s `:in`/`:within` options
```ruby
validates_length_of :first_name, in: ..30
```
*fatkodima*
* Add support for beginless ranges to inclusivity/exclusivity validators:
```ruby
validates_inclusion_of :birth_date, in: -> { (..Date.today) }
```
*Bo Jeanes*
* Make validators accept lambdas without record argument
```ruby
# Before
validates_comparison_of :birth_date, less_than_or_equal_to: ->(_record) { Date.today }
# After
validates_comparison_of :birth_date, less_than_or_equal_to: -> { Date.today }
```
*fatkodima*
* Fix casting long strings to `Date`, `Time` or `DateTime`
*fatkodima*
* Use different cache namespace for proxy calls
Models can currently have different attribute bodies for the same method
names, leading to conflicts. Adding a new namespace `:active_model_proxy`
fixes the issue.
*Chris Salzberg*
2021-12-07 23:52:30 +08:00
Please check [7-0-stable](https://github.com/rails/rails/blob/7-0-stable/activemodel/CHANGELOG.md) for previous changes.