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 ```
3.6 KiB
-
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:
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 expensivecast
operations will likely see greater improvements.Jonathan Hefner
-
has_secure_password
now supports password challenges via apassword_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 persistedpassword_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:
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:
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 thepassword_challenge
field just as you would for other form fields, including utilizingconfig.action_view.field_error_proc
.Jonathan Hefner
-
Support infinite ranges for
LengthValidator
s:in
/:within
optionsvalidates_length_of :first_name, in: ..30
fatkodima
-
Add support for beginless ranges to inclusivity/exclusivity validators:
validates_inclusion_of :birth_date, in: -> { (..Date.today) }
Bo Jeanes
-
Make validators accept lambdas without record argument
# 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
orDateTime
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
Please check 7-0-stable for previous changes.