This commit ports `ActiveRecord::AttributeMethods::BeforeTypeCast` to
`ActiveModel::BeforeTypeCast` and includes it in `ActiveModel::Attributes`.
Thus, classes that include `ActiveModel::Attributes` will now
automatically define methods such as `*_before_type_cast`, just as
Active Record models do.
The `ActiveRecord::AttributeMethods::BeforeTypeCast` module is kept for
backward compatibility, but it now merely includes
`ActiveModel::BeforeTypeCast`.
Co-authored-by: Petrik <petrik@deheus.net>
This moves `type_for_attribute` from `ActiveRecord::ModelSchema::ClassMethods`
to `ActiveModel::AttributeRegistration::ClassMethods`, where
`attribute_types` is also defined.
Co-authored-by: Petrik <petrik@deheus.net>
Follow-up to #46282.
The purpose of the optimization from #46282 is to avoid unnecessary
`deserialize` / `cast` / `serialize` calls associated with invoking
`value_for_database` on an attribute that has not changed. `dup`ing the
attribute accomplishes that, but `dup`ing the attribute's value is not
appropriate for some value types. For example, a value of the type
`ActiveModel::Type::ImmutableString` requires `clone` instead of `dup`,
and a value of the type `ActiveRecord::Type::Json` likely requires
`deep_dup`. In some cases the only appropriate method may be
`deserialize(serialize(value))`, such as when a serializer for the type
`ActiveRecord::Type::Serialized` deserializes `ActiveRecord::Base`
instances. (In that case, `dup`ing the value would clear its `id`, and
`clone`ing the value would only produce a shallow copy.) However,
`deserialize(serialize(value))` is expensive and would defeat the
purpose of the optimization.
Instead of `dup`ing the attribute, this commit changes the optimization
to use `with_value_from_database(value_before_type_cast)`, which
parallels `with_value_from_database(value_for_database)` in the base
implementation. This drops the (cast) value entirely, causing a fresh
copy to be deserialized if the attribute is subsequently read. In cases
where the attribute is _not_ subsequently read, this will actually be
more efficient since no extra work is performed. And in cases where the
attribute _is_ subsequently read, it will still be more efficient than
`deserialize(serialize(value))`.
Fixes#49809.
This adds a `decorate_attributes` method to Active Model to support
attribute type decoration. `decorate_attributes` is a private,
low-level API that is intended to be wrapped by high-level APIs like
`ActiveRecord::Base::normalizes` and `ActiveRecord::Base::enum`.
This change would force a lot of existing applications and libraries
to update their tests.
We included it in the beta to collect feedback from the community and
we had some comments about how negative this change would be.
Developers that care about the typography of their error messages
can easily change it in their applications using the translation
files, so there is no need to inflict pain in the upgrade process
by changing the default in the framework.
Revert "Merge PR #45463"
This reverts commit 9f60cd8dc7d963b1843b66d9639715b4a04c9c65, reversing
changes made to 35d574dbfda68d09fe1fb532f45a3e32f14c571d.
There are assertions that expected/actual arguments are passed in the
reversed order by mistake. Enabling the LiteralAsActualArgument rule
prevents this mistake from happening.
The existing tests were auto-corrected by rubocop with a bit of
indentation adjustment.
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
`undefine_attribute_methods` now removes alias attribute methods along
with attribute methods. This commit changes `define_attribute_methods` to
redefine methods back if any alias attributes were declared which provides
applications and libraries an option to bring the alias methods back
after using `undefine_attribute_methods`.
The current implementation of _to_partial_path composes the part of two bits, `collection' and `element'.
ActiveModel::Name also contains these fields, and they are derived the same way _to_partial_path does it.
However, _to_partial_path doesn't use the information in model_name, and solely relies on its own computations instead.
This works for all standard cases, but not necessarily for models that provide a non-standard model_name.
This commit fixes that and has _to_partial_path use model_name if the class responds to it.
This commit suppresses the `DEPRECATION WARNING: Support for `config.active_support.cache_format_version = 6.1`
has been deprecated and will be removed in Rails 7.2.` warning at `RailtieTest`
This commit sets `config.active_support.cache_format_version = 7.1` explicitly for `RailtieTest`
because https://github.com/rails/rails/pull/48598 deprecates `active_support.cache_format_version = 6.1` and still the default format_version is 6.1, I think this is intended.
4ac237de74/activesupport/lib/active_support/cache.rb (L55)
```
@format_version = 6.1
```
\### Steps to reproduce
```
git clone https://github.com/rails/rails
cd rails/activemodel
bundle
bin/test test/cases/railtie_test.rb:21
```
\### Without this commit
```
$ bin/test test/cases/railtie_test.rb:21
Run options: --seed 36872
\# Running:
DEPRECATION WARNING: Support for `config.active_support.cache_format_version = 6.1` has been deprecated and will be removed in Rails 7.2.
Check the Rails upgrade guide at https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#new-activesupport-cache-serialization-format
for more information on how to upgrade.
(called from block (3 levels) in run at /home/yahonda/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/minitest-5.19.0/lib/minitest/test.rb:94)
.
Finished in 0.321429s, 3.1111 runs/s, 3.1111 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
$
```
\### With this commit
```
$ bin/test test/cases/railtie_test.rb:21
Run options: --seed 65282
\# Running:
Finished in 0.006108s, 0.0000 runs/s, 0.0000 assertions/s.
0 runs, 0 assertions, 0 failures, 0 errors, 0 skips
$
```
This commit allows customizing the delimiter used by `to_param` when
`to_key` returns multiple value. This unblocks supporting more varieties
of composite primary key types in Active Record.
In `1818beb3a3ea5fdb498095d4885f8a7e512f24ca` Rails changed the target
where alias attribute methods are defined. It lead to
`undefine_attribute_methods` to clean alias attribute methods along with
the attribute methods. It was an intended behavior change but it wasn't
properly documented and tested. This commit clarifies the new behavior
in the Active Model changelog along with covering the behavior with tests.
This commit adds support for composite identifiers in `to_key`.
Rails 7.1 adds support for composite primary key which means that
composite primary key models' `#id` method returns an `Array` and
`to_key` needs to avoid double-wrapping the value.
If the user defined a translation to a nested error on base we should
look it up in the same way we do for the other attributes.
If no translation is set, we fallback to the name of the association.
Fixes#48884.
ActiveRecord::Base has a dedicated ActiveSupport load hook. This adds an
additional hook for ActiveModel::Model, so that when ActiveModel is
being used without ActiveRecord, it can still be modified.
JSONGemEncoder.encode previously would always perform two passes. First
it would call `.as_json(options)`, but then would perform a second pass
"jsonify" to recursively call `.as_json` (this time without options)
until the data converges into a "JSON-ready" representation.
When options are not given, the second pass should be equivalent to the
first, so we can detect that, and only perform the "jsonify" step.
The only user-visible effect of this should be that we will pass no
options to `as_json` instead of an empty Hash, but implementations of
`as_json` should already be expected to accept that.
This commit adds an `after_teardown` logic to the `activemodel` test
suite which ensures that performed test incremented the `assertions`
counter at least once. Otherwise it raises an `AssertionlessTest` error.
This leads to a requirement for tests to be verbose about assertions
even if technically it may not be needed. For example a test like:
```ruby
def test_submitting_a_review_doesnt_raise
review.submit!
end
```
will have to at least become
```ruby
def test_submitting_a_review_doesnt_raise
assert_nothing_raised { review.submit! }
end
```
or preferably it should perform a semantically meaningful assertion
that will imply not exception being raised, for example:
```ruby
def test_submitting_a_review_doesnt_raise
review.submit!
assert_not_nil review.submitted_at
end
```
Overall while the requirement is being defensive it improves
readability of the tests along with ensuring that we will never
end up having tests that test nothing.
- Simplify password validation to only check byte size for BCrypt limit (72 bytes)
- Replace specific error messages with a single "is too long" message
- Update test cases to reflect new error message
Co-authored-by: ChatGPT
- Validate password length in both characters and bytes
- Provide user-friendly error message for character length
- Add byte size validation due to BCrypt's 72-byte limit
Co-authored-by: ChatGPT
[Fix#47600]
With the universal adoption of UTF-8 in browsers, user facing text can
use more optimal Unicode typography. In digital and print design, using
RIGHT SINGLE QUOTATION MARK (U+2019) is normally preferred over
APOSTROPHE (U+0027) in contractions.
For details, see the Unicode Standard Section 6.2:
https://www.unicode.org/versions/Unicode13.0.0/ch06.pdf
> Punctuation Apostrophe. U+2019 right single quotation mark is
> preferred where the character is to represent a punctuation mark, as
> for contractions: “We’ve been here before.” In this latter case,
> U+2019 is also referred to as a punctuation apostrophe.
Right now since we have instance variable called `itself_if_serialize_cast_value_compatible`
assigned to self when we run `as_json` we get stack too deep error because `as_json` calls
`as_json` on every instance variable. And since `@itself_if_serialize_cast_value_compatible` references
to `self` we run into recursion.
And before that we were returning unpredictable data from `as_json` method so it's better to let it to throw
an error and let user know that Value class is not supposed to be converted to json.
This commit adds `ActiveModel.deprecator`, and adds it to
`Rails.application.deprecators` so that it can be configured via
settings such as `config.active_support.report_deprecations`.
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
```
We recently let a few very easy to avoid warnings get merged.
The root cause is that locally the test suite doesn't run in
verbose mode unless you explictly pass `-w`.
On CI warnings are enabled, but there is no reason to look at the
build output unless something is failing. And even if one wanted
to do that, that would be particularly work intensive since warnings
may be specific to a Ruby version etc.
Because of this I believe we should:
- Always run the test suite with warnings enabled.
- Raise an error if a warning is unexpected.
We've been using this pattern for a long time at Shopify both in private
and public repositories.
Most model attribute types try to cast a given value before serializing
it. This allows uncast values to be passed to finder methods and still
be serialized appropriately. However, when persisting a model, this
cast is unnecessary because the value will already have been cast by
`ActiveModel::Attribute#value`.
To eliminate the overhead of a 2nd cast, this commit introduces a
`ActiveModel::Type::SerializeCastValue` module. Types can include this
module, and their `serialize_cast_value` method will be called instead
of `serialize` when serializing an already-cast value.
To preserve existing behavior of any user types that subclass Rails'
types, `serialize_after_cast` will only be called if the type itself
(not a superclass) includes `ActiveModel::Type::SerializeCastValue`.
This also applies to type decorators implemented via `DelegateClass`.
Benchmark script:
```ruby
require "active_model"
require "benchmark/ips"
class ActiveModel::Attribute
alias baseline_value_for_database value_for_database
end
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",
}
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
TYPES.each do |name, type|
$attribute_set ||= MyModel.new(VALUES).instance_variable_get(:@attributes)
attribute = $attribute_set[name.to_s]
puts "=" * 72
Benchmark.ips do |x|
x.report("#{type} before") { attribute.baseline_value_for_database }
x.report("#{type} after") { attribute.value_for_database }
x.compare!
end
end
```
Benchmark results:
```
========================================================================
Warming up --------------------------------------
big_integer before 100.417k i/100ms
big_integer after 260.375k i/100ms
Calculating -------------------------------------
big_integer before 1.005M (± 1.0%) i/s - 5.121M in 5.096498s
big_integer after 2.630M (± 1.0%) i/s - 13.279M in 5.050387s
Comparison:
big_integer after: 2629583.6 i/s
big_integer before: 1004961.2 i/s - 2.62x (± 0.00) slower
========================================================================
Warming up --------------------------------------
boolean before 230.663k i/100ms
boolean after 299.262k i/100ms
Calculating -------------------------------------
boolean before 2.313M (± 0.7%) i/s - 11.764M in 5.085925s
boolean after 3.037M (± 0.6%) i/s - 15.262M in 5.026280s
Comparison:
boolean after: 3036640.8 i/s
boolean before: 2313127.8 i/s - 1.31x (± 0.00) slower
========================================================================
Warming up --------------------------------------
date before 148.821k i/100ms
date after 298.939k i/100ms
Calculating -------------------------------------
date before 1.486M (± 0.6%) i/s - 7.441M in 5.006091s
date after 2.963M (± 0.8%) i/s - 14.947M in 5.045651s
Comparison:
date after: 2962535.3 i/s
date before: 1486459.4 i/s - 1.99x (± 0.00) slower
========================================================================
Warming up --------------------------------------
datetime before 92.818k i/100ms
datetime after 136.710k i/100ms
Calculating -------------------------------------
datetime before 920.236k (± 0.6%) i/s - 4.641M in 5.043355s
datetime after 1.366M (± 0.8%) i/s - 6.836M in 5.003307s
Comparison:
datetime after: 1366294.1 i/s
datetime before: 920236.1 i/s - 1.48x (± 0.00) slower
========================================================================
Warming up --------------------------------------
decimal before 50.194k i/100ms
decimal after 298.674k i/100ms
Calculating -------------------------------------
decimal before 494.141k (± 1.4%) i/s - 2.510M in 5.079995s
decimal after 3.015M (± 1.0%) i/s - 15.232M in 5.052929s
Comparison:
decimal after: 3014901.3 i/s
decimal before: 494141.2 i/s - 6.10x (± 0.00) slower
========================================================================
Warming up --------------------------------------
float before 217.547k i/100ms
float after 298.106k i/100ms
Calculating -------------------------------------
float before 2.157M (± 0.8%) i/s - 10.877M in 5.043292s
float after 2.991M (± 0.6%) i/s - 15.203M in 5.082806s
Comparison:
float after: 2991262.8 i/s
float before: 2156940.2 i/s - 1.39x (± 0.00) slower
========================================================================
Warming up --------------------------------------
immutable_string before
163.287k i/100ms
immutable_string after
298.245k i/100ms
Calculating -------------------------------------
immutable_string before
1.652M (± 0.7%) i/s - 8.328M in 5.040855s
immutable_string after
3.022M (± 0.9%) i/s - 15.210M in 5.033151s
Comparison:
immutable_string after: 3022313.3 i/s
immutable_string before: 1652121.7 i/s - 1.83x (± 0.00) slower
========================================================================
Warming up --------------------------------------
integer before 115.383k i/100ms
integer after 159.702k i/100ms
Calculating -------------------------------------
integer before 1.132M (± 0.8%) i/s - 5.769M in 5.095041s
integer after 1.641M (± 0.5%) i/s - 8.305M in 5.061893s
Comparison:
integer after: 1640635.8 i/s
integer before: 1132381.5 i/s - 1.45x (± 0.00) slower
========================================================================
Warming up --------------------------------------
string before 163.061k i/100ms
string after 299.885k i/100ms
Calculating -------------------------------------
string before 1.659M (± 0.7%) i/s - 8.316M in 5.012609s
string after 2.999M (± 0.6%) i/s - 15.294M in 5.100008s
Comparison:
string after: 2998956.0 i/s
string before: 1659115.6 i/s - 1.81x (± 0.00) slower
========================================================================
Warming up --------------------------------------
time before 98.250k i/100ms
time after 133.463k i/100ms
Calculating -------------------------------------
time before 987.771k (± 0.7%) i/s - 5.011M in 5.073023s
time after 1.330M (± 0.5%) i/s - 6.673M in 5.016573s
Comparison:
time after: 1330253.9 i/s
time before: 987771.0 i/s - 1.35x (± 0.00) slower
```
Rubinius has not been maintained since May 2020 and based on the
discussion at https://github.com/rails/rails/pull/44984 ,
I think we can remove Rubinius specific code from Rails.
I was running into a case where I didn't want to just disabled the
validations and add my own. In fact, I would very much like to keep the
default validation but just de-activate it on some scenario:
e.g. Inviting a user without having to set a password for them yet so
they can add it themselves later when they receive an email invitation
to finish setting up their account.
My understanding of the validations flag originally intended was to
just disabled them and if you needed something more custom, you could
run your own validations instead.
This would be an acceptable solution, but it would add more code to my
controller. Instead validations can receive a `Hash` wich is then use to
apply validations rules to `validate`.
This is just a suggestion, I am not sure if there is a need, and I am
aware this PR is probably far from perfect. Any feedback welcome.
EDIT: implemented changes as per feedback.