2022-11-21 03:36:29 +08:00
|
|
|
* 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*
|
|
|
|
|
2022-07-26 01:16:46 +08:00
|
|
|
* `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*
|
|
|
|
|
2022-05-20 07:13:09 +08:00
|
|
|
* Support infinite ranges for `LengthValidator`s `:in`/`:within` options
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
validates_length_of :first_name, in: ..30
|
|
|
|
```
|
|
|
|
|
|
|
|
*fatkodima*
|
|
|
|
|
2022-05-18 12:57:15 +08:00
|
|
|
* Add support for beginless ranges to inclusivity/exclusivity validators:
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
validates_inclusion_of :birth_date, in: -> { (..Date.today) }
|
|
|
|
```
|
|
|
|
|
|
|
|
*Bo Jeanes*
|
|
|
|
|
2022-05-18 01:46:00 +08:00
|
|
|
* 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*
|
|
|
|
|
2022-05-03 00:59:50 +08:00
|
|
|
* Fix casting long strings to `Date`, `Time` or `DateTime`
|
|
|
|
|
|
|
|
*fatkodima*
|
|
|
|
|
2022-02-03 16:17:03 +08:00
|
|
|
* Use different cache namespace for proxy calls
|
2021-11-17 03:56:30 +08:00
|
|
|
|
2022-02-03 16:17:03 +08:00
|
|
|
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-11-17 03:56:30 +08:00
|
|
|
|
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.
|