Commit Graph

1998 Commits

Author SHA1 Message Date
Nikita Vasilevsky 5dbc7b424e
Use `call_args` in the `define_proxy_method` namespace.
`define_proxy_call` accepts `call_args` as an argument which impacts
method body generation but it doesn't use `call_args` in the `namespace`
generation which leads to the same cached method to be reused even if
`call_args` differ.

It has never been an issue since `define_proxy_call` was only used to
generate Active Record attribute methods and we were always passing
the same `call_args` per method name.

However, since https://github.com/rails/rails/pull/48533 we are using
`define_proxy_call` to generate alias attribute methods where `call_args`
differ for the same method name which leads to the same cached method
being reused in wrong places.

This commit fixes the issue by making sure `call_args` are being
considered when generating the `namespace` for the method.
2023-07-28 19:28:55 +00:00
Jean Boussier 3ee73bff39
Merge pull request #48533 from Shopify/delay-alias-attribute-defition
Call proxy methods from `alias_attribute` generated methods
2023-07-21 14:08:30 +02:00
Ufuk Kayserilioglu c2b195e1e3
Change load error messages to use `Kernel#warn` instead of `$stderr.puts`
When development tools try to load Rails components, they sometimes end up loading files that will error out since a dependency is missing. In these cases, the tooling can catch the error and change its behaviour.

However, since the warning is printed directly to `$stderr`, the tooling cannot catch and suppress it easily, which ends up causing noise in the output of the tool.

This change makes Rails print these warnings using `Kernel#warn` instead, which can be suppressed by the tooling.
2023-07-21 00:38:12 +03:00
Nikita Vasilevsky 1818beb3a3
Call proxy methods from `alias_attribute` generated methods
This commit changes bodies of methods generated by `alias_attribute`
along with generating these methods lazily.

Previously the body of the `alias_attribute :new_title, :title` was
`def new_title; title; end`. This commit changes it to
`def new_title; attribute("title"); end`.

This allows for `alias_attribute` to be used to alias attributes named
with a reserved names like `id`:
```ruby
  class Topic < ActiveRecord::Base
    self.primary_key = :title
    alias_attribute :id_value, :id
  end

  Topic.new.id_value # => 1
```
2023-07-17 22:22:28 +00:00
Jean Boussier 29205d7928 Eagerly cast serialized query attributes
Fix: https://github.com/rails/rails/issues/48652
Ref: https://github.com/rails/rails/pull/46048
Ref: https://github.com/rails/rails/issues/46044
Ref: https://github.com/rails/rails/pull/34303
Ref: https://github.com/rails/rails/pull/39160
Close: https://github.com/rails/rails/pull/48705

This deep_dup was introduced to prevent the value stored in the query
cache to be later mutated.

The problem is that `ActiveRecord::Base#dup` will return a copy
of the record but with the primary key set to nil. One could
argue that `#dup` shouldn't behave this way, but I think this ship
has sailed (or has it?).

My initial fix was to instead always call `type.cast` eagerly so that we'd dup
serialized types in a more correct way. However there is a test
that explictly ensure this doesn't happen: https://github.com/rails/rails/pull/39160

The reason isn't 100% clear to me, but if I get it correctly, it's to avoid
a potentially costly operation upfront.

So instead we only eagerly cast serialized attributes only, so protect against
future mutations.

Mutable types are still deep duped.
2023-07-13 11:44:33 +02:00
Lewis Buckley 08a79ce284
Add a load hook for `ActiveModel::Model`
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.
2023-07-09 13:08:34 +01:00
Guillermo Iguaran b790387597
Merge pull request #48616 from p8/activerecord/document-dirty
Document `ActiveRecord::Dirty` module in the API docs
2023-06-30 13:07:13 -07:00
Petrik 469dc61706 Fix some formatting in ActiveModel::Dirty docs 2023-06-30 21:05:01 +02:00
Petrik 4b3920ac9e Document `ActiveRecord::AttributeMethods::Dirty` module in the API docs
Currently it is a bit unclear which dirty methods can be called on
Active Record models. You have to know that methods from ActiveModel::Dirty
are included.

It also unclear if methods can be invoked in the form of
`saved_change_to_name?` unless you read the documentation of the
`saved_change_to_attribute?` method.

By adding an introduction to the module we can show which methods are
defined specifically for Active Record, and how to call them, very
similar to the ActiveModel::Dirty introduction.
Linking to ActiveModel::Dirty makes it's also easier to find methods
defined there.
2023-06-30 11:02:16 +02:00
Petrik ee58c8e6be Document generated dirty attribute methods [ci-skip]
Active Model generates methods for each attribute to handle dirty
changes. As these methods are generated they can't be found when
searching for them in the API documentation.

We can use the `:method:` directive to document them in the form of
`*_will_change`, `*_previously_was`, etc.
This notation is already used for documenting ActiveModel::Dirty.

An alternative could be documenting the methods as:
`[attribute_name]_will_change`, `[attribute_name]_previously_was`, etc...
But that adds more noise to the method.
2023-06-29 22:19:12 +02:00
zzak dd89f600f7
🔗 Remove RDoc auto-link from Rails module everywhere 2023-06-23 10:49:30 +09:00
Matthew Draper 308dd9c504 Ensure binary-destined values have binary encoding during type cast 2023-05-29 18:21:20 +09:30
zzak d7d24b02c1
Fix linking on ActiveModel::API 2023-05-29 12:40:48 +09:00
zzak 8674d53cd0
Fix heading for StrictValidationFailed 2023-05-27 06:56:26 +09:00
zzak 38bef29064
Replace all occurrences of '<tt>(\w+::\w+::\w+)</tt>' with '+$1+'
E.g.:

* <tt>Rails::Command::NotesCommand</tt> -> +Rails::Command::NotesCommand+

Co-authored-by: Hartley McGuire <skipkayhil@gmail.com>
2023-05-25 06:56:17 +09:00
zzak e3c73fd183
Replace all occurrences of '<tt>(\w+::\w+)</tt>' with '+$1+'
E.g.:

* <tt>ActiveRecord::Base</tt> -> +ActiveRecord::Base+

Co-authored-by: Hartley McGuire <skipkayhil@gmail.com>
Co-authored-by: Petrik de Heus <petrik@deheus.net>
2023-05-25 06:52:32 +09:00
Jean Boussier 108617eb74 Fix change_in_place? for binary serialized columns
Followup: https://github.com/rails/rails/pull/40383
Fix: https://github.com/rails/rails/issues/48255
Fix: https://github.com/rails/rails/pull/48262

If the serialized attribute is backed by a binary column, we must ensure
that both the `raw_old_value` and the `raw_new_value` are casted to
`Binary::Data`.

Additionally, `Binary::Data` must cast it's backing string in
`Encoding::BINARY` otherwise comparison of strings containing bytes
outside the ASCII range will fail.
2023-05-22 13:01:42 +02:00
zzak 0ec9e750ea
Merge pull request #48083 from skipkayhil/document-dirty-methods
Document ActiveModel::Dirty dispatch targets [ci skip]
2023-05-19 06:39:25 +09:00
Eileen M. Uchitelle bc34f356e3
Merge pull request #48177 from eileencodes/unrevert-46444
Revert "Merge pull request #46444 from eileencodes/revert-forgetting-…
2023-05-09 13:29:28 -04:00
Jonathan Hefner d6da86a476 Prevent non-anonymous modules from becoming frozen
In #48106, `Module#deep_dup` was changed to return the module itself
(not a copy) when the module is not anonymous.  However, that causes
non-anonymous modules to become frozen via `value.deep_dup.freeze` when
passed to `ActiveModel::Type::Helpers::Mutable#immutable_value`.  So,
for example, class attributes can no longer be set on the module.

To prevent such issues, this commit removes the `freeze` from
`immutable_value`.  `immutable_value` is only called by
`ActiveRecord::PredicateBuilder#build_bind_attribute`, which only cares
that other code cannot mutate the value, not that the value is actually
frozen.
2023-05-09 10:51:00 -05:00
eileencodes 38141681cc
Revert "Merge pull request #46444 from eileencodes/revert-forgetting-assignment-changes"
This reverts commit 586436d370, reversing
changes made to 866e053732.

This is an unrevert of https://github.com/rails/rails/pull/46282. I
should have reverted the revert sooner but it kept falling off my radar.

Closes #46446
2023-05-09 11:19:12 -04:00
Jean Boussier b717a5a0d4 Revert "Merge pull request #47352 from basecamp/ar-freeze-cached"
This reverts commit 2c20f90eba, reversing
changes made to 912096d4ce.
2023-05-02 10:53:14 +02:00
Jorge Manrubia f195e033ac Freeze casted values instead of original values in database queries
This deals with a problem introduced in #7743ab95b8e15581f432206245c691434a3993d1a751b9d451170956d59457a9R8
that was preventing query `Class` serialized attributes. Duplicating the original
`Class` argument generates an anonymous class that can't be serialized as YAML.

This change makes query attributes hasheable based on their frozen casted values
to prevent the problem.

This solution is based on an idea by @matthewd from https://github.com/rails/rails/issues/47338#issuecomment-1424402777.
2023-05-02 10:09:20 +02:00
Hartley McGuire 7c1a5639c4
Document ActiveModel::Dirty dispatch targets
As part of my Railsconf talk I mentioned that attribute_changed? was an
undocumented method. Sage was surprised by this, and suggested that we
should probably be documenting this, since *_changed? methods are part
of the public API of Active Model.

I discussed this further with Rafael at Railsconf and we believe that in
addition to attribute_changed?, we should probably be documenting the
other dispatch targets in ActiveModel::Dirty as well.

Co-authored-by: Sage Griffin <sage@sagetheprogrammer.com>
2023-04-27 14:45:05 -04:00
Petrik de Heus f2327e8df1
Merge pull request #48024 from p8/docs/headers-active-model-types
Fix ActiveModel type headings [ci-skip]
2023-04-22 12:09:47 +02:00
Petrik de Heus 0d02e5d4c3
Merge pull request #48022 from p8/docs/fix-header-level
Fix header level for ActiveModel::Naming and DelegatedType
2023-04-22 12:08:52 +02:00
Petrik 5a3daa626d Fix ActiveModel type headings [ci-skip] 2023-04-22 12:02:41 +02:00
Petrik 17b3e05133 Fix header level for ActiveModel::Naming and DelegatedType
A class should have a h1 instead of a h2.
2023-04-22 11:25:27 +02:00
Petrik 3042111944 Add some missing headers to Active Model Type docs [ci-skip] 2023-04-22 11:22:16 +02:00
Guillermo Iguaran a60785cc27 Update password validation and error messages
- 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
2023-04-19 15:57:51 -07:00
Guillermo Iguaran 74264f4467 Improve password length validation in ActiveModel::SecurePassword for BCrypt compatibility
- 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]
2023-04-19 15:56:19 -07:00
Petrik 62c6cb3449 Fix more missing Active Model documentation headers [ci-skip]
This adds headers to the remaining Active Model Errors.
2023-04-01 15:46:04 +02:00
Petrik f8e5dab6a4 Use h1 for Active Model documentation titles [ci-skip]
Most of the other frameworks use a h1(`=`) instead of h2(`==`) for
class/module documentation. Having a h1 will improve SEO and makes
things look more consistent.

This also adds a missing title and escapes a namespace so it won't be
linked.
2023-03-31 13:07:31 +02:00
Petrik de Heus dc0f20595d
Merge pull request #47717 from p8/docs/include-readmes
Include READMEs in main framework pages of the API documentation
2023-03-30 16:43:14 +02:00
Jean Boussier fcff4c8b4f ActiveModel::Type::SerializeCastValue only compute compatibility once per class
All instances of the same type will return the same thing, so we
might as well save some work and save an ivar.
2023-03-29 15:07:39 +02:00
Petrik 7c94708d24 Include READMEs in main framework pages of the API documentation
Currently when opening the main framework pages there is no introduction
to the framework. Instead we only see a whole lot of modules and the
`gem_version` and `version` methods.

By including the READMEs using the `:include:` directive each frameworks
has a nice introduction.
For markdown READMEs we need to add the :markup: directive.

[ci-skip]

Co-authored-by: zzak <zzakscott@gmail.com>
2023-03-21 21:16:28 +01:00
Matthew Draper bb7f3be138
Revert "Quote binary strings in Arel" 2023-03-15 19:50:50 +10:30
Jonathan Hefner db8f664b50
Merge pull request #47490 from lazaronixon/has_secure_password_salt
Enhance has_secure_password to also generate a password_salt method

Co-authored-by: Guillermo Iguaran <guilleiguaran@gmail.com>
2023-03-09 14:53:10 -06:00
Nixon ebe9e575b7 Enhance has_secure_password to also generate a password_salt method 2023-03-09 16:44:10 -03:00
Ole Friis Østergaard f4242739aa Move SQLite3 blob encoding to ActiveModel 2023-03-09 09:23:00 +00:00
Rafael Mendonça França 9f60cd8dc7
Merge PR #45463 2023-03-03 22:58:01 +00:00
Petrik 661c995f3b Add class name to ActiveModel::MissingAttributeError error message.
When an attribute is missing the current message is unclear about which
class is missing the attribute, especially when there are multiple
classes that could miss the attribute.

By adding the classs name to the error message it is easier to debug:

```ruby
user = User.first
user.pets.select(:id).first.user_id
=> ActiveModel::MissingAttributeError: missing attribute 'user_id' for Pet
```

This also makes the error message more inline with the
UnknownAttributeError message:

```ruby
=> ActiveModel::UnknownAttributeError: unknown attribute 'name' for Person
```

Co-authored-by: Yasuo Honda <yasuo.honda@gmail.com
2023-03-03 14:31:22 +01:00
joernchen of Phenoelit 2030530865 Fix issue with attr_protected where malformed input could circumvent
protection

Fixes: CVE-2013-0276
2023-02-26 22:20:26 +01:00
Jon Dufresne da82e587f2 Improve typography of user facing validation messages
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.
2023-02-25 08:21:19 -08:00
zzak d2af670dba
Remove Copyright years (#47467)
* Remove Copyright years

* Basecamp is now 37signals... again

Co-authored-by: David Heinemeier Hansson <dhh@hey.com>

---------

Co-authored-by: David Heinemeier Hansson <dhh@hey.com>
2023-02-23 11:38:16 +01:00
Yasuo Honda f838a74212
Merge pull request #46866 from ghousemohamed/change-year-2022-to-2023 2023-02-13 13:15:43 +09:00
Étienne Barrié c958bc1342 Remove unparsed :method directive
RDoc doesn't parse this directive since the method is defined below.
It's only useful for metaprogrammed methods.

https://github.com/ruby/rdoc/blob/v6.5.0/lib/rdoc/parser/ruby.rb#L138-L139
2023-02-07 13:15:07 +01:00
zzak 8feec6d452 Tiny knits from AM::Validator docs 2023-02-06 16:39:00 +09:00
Jean Boussier aa7d78d9b1 Improve Rails' Shape friendliness (second pass)
Followup: https://github.com/rails/rails/pull/47023

```
Shape Edges Report
-----------------------------------
snip...
       238  @errors
snip...
       219  @options
snip...
       129  @_request
       128  @type
       125  @virtual_path
       124  @_assigns
       123  @_config
       123  @_controller
       123  @output_buffer
       123  @view_flow
       122  @_default_form_builder
snip...
        89  @_already_called
        75  @validation_context
snip...
        65  @_new_record_before_last_commit
snip...
        58  @_url_options
snip...
```
2023-01-17 13:55:49 +01:00
Rafael Mendonça França a52596c943
Merge pull request #47022 from zzak/callbacks-links-continued
Fix more links for callbacks
2023-01-16 15:38:27 -05:00
Jean Boussier fc950324bd Improve Rails' Shape friendliness
Ruby 3.2 significantly changed how instance variables are store.
It now use shapes, and in short, it's important for performance
to define instance variables in a consistent order to limit the
amount of shapes.

Otherwise, the number of shapes will increase past a point where
MRI won't be able to cache instance variable access. The impact
is even more important when YJIT is enabled.

This PR is data driven. I dump the list of Shapes from Shopify's
monolith production environment, and Rails is very present among
the top offenders:

```
Shape Edges Report
-----------------------------------
       770  @default_graphql_name
       697  @own_fields
       661  @to_non_null_type
       555  @own_interface_type_memberships
       472  @description
       389  @errors
       348  @oseid
       316  @_view_runtime
       310  @_db_runtime
       292  @visibility
       286  @shop
       271  @attribute_method_patterns_cache
       264  @namespace_for_serializer
       254  @locking_column
       254  @primary_key
       253  @validation_context
       244  @quoted_primary_key
       238  @access_controls
       234  @_trigger_destroy_callback
       226  @_trigger_update_callback
       224  @finder_needs_type_condition
       215  @_committed_already_called
       214  @api_type
       203  @mutations_before_last_save
       202  @access_controls_overrides
       201  @options
       198  @mutations_from_database
       190  @_already_called
       183  @name
       179  @_request
       176  @own_arguments
       175  @_assigns
       175  @virtual_path
       174  @context
       173  @_controller
       173  @output_buffer
       173  @view_flow
       172  @_default_form_builder
       169  @cache
       159  @_touch_record
       151  @attribute_names
       151  @default_attributes
       150  @columns_hash
       149  @attribute_types
       148  @columns
       147  @marked_for_same_origin_verification
       146  @schema_loaded
       143  @_config
       143  @type
       141  @column_names
```

All the changes are of similar nature, the goal is to preset the instance
variable to nil when objects are allocated, or when classes are created.

For classes I leverage the `inherited` hook. If the patern becomes common enough
it might make sense to add a helper for this in `ActiveSupport::Concern`.
2023-01-16 12:31:37 +01:00
zzak ae8ca668d8 Fix more links for callbacks
Some of these are linked unnecessarily, while some should be added.
2023-01-16 19:04:26 +09:00
Akira Matsuda bd11e520a2
Merge pull request #46868 from amatsuda/Time.new_string
Use Ruby 3.2's native ISO 8601-ish String parser to cast String to Time in Active Record
2023-01-11 03:56:25 +09:00
Jonathan Hefner 3eadf057db Fix typos in API docs [ci-skip] 2023-01-08 15:47:20 -06:00
Jonathan Hefner 33557c5dca Indent private methods in code examples [ci-skip]
This matches the indentation used in generated code, such as code from
`railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt`.
2023-01-08 15:47:20 -06:00
zzak 50507abe44
Merge pull request #46856 from zzak/validations-api-default-error-messages
[ci-skip] Document default error messages for some validations
2023-01-08 08:29:52 +09:00
Ghouse Mohamed e0559d2c1c Change 2022 -> 2023 2023-01-03 13:22:00 +05:30
Akira Matsuda 341b30be2e
Wrap broken Time object with Time.at for Ruby 3.2.0 bug
Time.new in: 'UTC' creates an internally broken Time object, hence we need to
recreate a valid Time object with the same value.
see: https://bugs.ruby-lang.org/issues/19292

This issue has been fixed on trunk Ruby, so this can be reverted in the future.

Benchmark:
new: after this patch
old: our own Time parsin hack

"UTC"
Warming up --------------------------------------
                 new   146.681k i/100ms
                 old   112.821k i/100ms
Calculating -------------------------------------
                 new      1.542M (± 0.9%) i/s -      7.750M in   5.025003s
                 old      1.193M (± 2.4%) i/s -      6.040M in   5.067234s

Comparison:
                 new:  1542382.4 i/s
                 old:  1192721.1 i/s - 1.29x  (± 0.00) slower

"non-UTC"
Warming up --------------------------------------
                 new    85.220k i/100ms
                 old    62.541k i/100ms
Calculating -------------------------------------
                 new    927.019k (± 0.8%) i/s -      4.687M in   5.056394s
                 old    640.220k (± 1.8%) i/s -      3.252M in   5.081417s

Comparison:
                 new:   927018.9 i/s
                 old:   640219.7 i/s - 1.45x  (± 0.00) slower
2023-01-02 21:14:35 +09:00
Akira Matsuda afa47b93a1
Use Ruby 3.2's native ISO 8601-ish String parser for speed and maintainability
Benchmark:

"UTC"
Warming up --------------------------------------
                 new   220.100k i/100ms
                 old   110.278k i/100ms
Calculating -------------------------------------
                 new      2.513M (± 3.0%) i/s -     12.766M in   5.084750s
                 old      1.182M (± 3.3%) i/s -      5.955M in   5.045163s

Comparison:
                 new:  2512925.8 i/s
                 old:  1181629.4 i/s - 2.13x  (± 0.00) slower

"non-UTC"
Warming up --------------------------------------
                 new    88.942k i/100ms
                 old    62.553k i/100ms
Calculating -------------------------------------
                 new    927.745k (± 2.9%) i/s -      4.714M in   5.085328s
                 old    637.047k (± 2.9%) i/s -      3.190M in   5.012170s

Comparison:
                 new:   927744.8 i/s
                 old:   637047.3 i/s - 1.46x  (± 0.00) slower
2023-01-02 15:59:16 +09:00
zzak b9aaf99701
Fix rdoc syntax for errors#add (#46857) 2022-12-31 10:34:02 +09:00
zzak 9de0d59bb7
Fix rdoc syntax for Kernel.Float (#46860) 2022-12-31 10:33:16 +09:00
zzak 29afcac53b Document default error messages for some validations 2022-12-30 09:56:51 +09:00
Yasuo Honda 40255e4af0
Merge pull request #46397 from shouichi/document-validation-context
Document ActiveModel#validation_context [skip ci]
2022-12-02 08:48:26 +09:00
Shouichi Kamiya 8be20b0deb Document ActiveModel#validation_context [skip ci]
Background:

To run validations except in a certain context, we need to either

1. explicitly pass all validation contexts to the `on` option
2. access undocumented `validation_context`

Option 1 is error-prone. Additionally, adding the opposite behavior of
`on` was rejected https://github.com/rails/rails/pull/30710.

Solution:

Document `ActiveModel#validation_context` to make it clear that the API is
public.

Close https://github.com/rails/rails/issues/46391.
2022-11-29 11:36:59 +09:00
Rafael Mendonça França 5df0f0da7c
Merge pull request #46535 from nashby/type-json
Fix `as_json` call on ActiveModel::Type's child classes.
2022-11-28 14:54:22 -05:00
Étienne Barrié 3d6a7b2faa Initialize deprecators before configuring them
Since engine initializers run later in the process, we need to run this
initializer earlier than the default.

This ensures they're all registered before the environments are loaded.
2022-11-28 10:47:26 +01:00
Vasiliy Ermolovich 433bd5995d Raise `NoMethodError` in `ActiveModel::Type::Value#as_json` method.
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.
2022-11-25 21:25:06 +01:00
fatkodima f2f3fa95ec Autoload `ActiveModel::ValidationError` 2022-11-22 12:36:04 +02:00
Jonathan Hefner 8d9f5257e3 Clarify numeric casting behavior for blank strings [ci-skip]
`ActiveModel::Type::Decimal`, `ActiveModel::Type::Float`, and
`ActiveModel::Type::Integer` cast blank strings to `nil` rather than
casting them with `to_d`, `to_f`, or `to_i`.  This commit clarifies that
behavior in each type's documentation.
2022-11-15 13:57:15 -06:00
Jonathan Hefner 06d37079e8 Document ActiveModel::Type::Time as time of day [ci-skip]
`ActiveModel::Type::Time` is meant to represent time of day (without a
date or time zone).  This commit fixes the code example to show that the
date and time zone are normalized when parsing a time from a string.
2022-11-15 13:57:15 -06:00
eileencodes dbb47d6abd
Revert "Merge pull request #46282 from jonathanhefner/active_model-forgetting_assignment-avoid-value_for_database"
This reverts commit 1f039d8f40, reversing
changes made to be0b5c65a1.

This revert is temporary while we debug some issues with our app. While
we're pretty sure this is caused by code in our application and a
compbination with `activerecord-typed_store`, the failure mode is easy
to miss because it doesn't raise an exception.

We will un-revert after a bit more investigation. We just want to be
confident that the other cases where this could cause issues are fixed
without being blocked from upgrading for the next month or so.
2022-11-08 13:03:01 -05:00
Jonathan Hefner 16ab507a4e
Merge pull request #46401 from jonathanhefner/other-framework-deprecators
Add other framework deprecators
2022-11-03 17:10:05 -05:00
Sébastien Puyet 5e3b9f27ce Fix active model errors add documentation 2022-11-03 17:22:11 +01:00
Jonathan Hefner 74794858c9 Add ActiveModel.deprecator
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`.
2022-11-01 17:39:39 -05:00
Jonathan Hefner 98aa59a7f6 Avoid value_for_database if attribute not updated
After a record is saved, `ActiveModel::Attribute#forgetting_assignment`
is called on each of its attributes.  `forgetting_assignment`, in turn,
calls `ActiveModel::Attribute#value_for_database`.  If an attribute was
not updated, and therefore `value_for_database` was not previously
computed, this will involve an unnecessary serialize (and cast,
depending on type).  And if the attribute was not previously read, it
will also involve an unnecessary deserialize (and cast, depending on
type).

This commit overrides `FromDatabase#forgetting_assignment` to dup the
attribute instead of computing `value_for_database` in the case where
the attribute was not updated.  This can improve the performance for
basic types:

**Benchmark script**

  ```ruby
  # frozen_string_literal: true
  require "benchmark/ips"

  ActiveModel::Attribute.alias_method :baseline_forgetting_assignment, :forgetting_assignment

  {
    big_integer: 123456,
    boolean: 1,
    date: "1999-12-31",
    datetime: "1999-12-31 12:34:56.789011",
    decimal: 123.456,
    float: 123.456,
    immutable_string: "abcdef",
    integer: 123456,
    string: "abcdef",
    time: "1999-12-31 12:34:56.789011",
  }.each do |type_name, value_before_type_cast|
    puts "=== #{type_name} ".ljust(70, "=")

    type = ActiveModel::Type.lookup(type_name)

    Benchmark.ips do |x|
      x.report("before") do
        attribute = ActiveModel::Attribute.from_database("x", value_before_type_cast, type)
        attribute.baseline_forgetting_assignment
      end

      x.report("after") do
        attribute = ActiveModel::Attribute.from_database("x", value_before_type_cast, type)
        attribute.forgetting_assignment
      end

      x.compare!
    end

    Benchmark.ips do |x|
      x.report("before w/ read") do
        attribute = ActiveModel::Attribute.from_database("x", value_before_type_cast, type)
        attribute.value
        attribute.baseline_forgetting_assignment
      end

      x.report("after w/ read") do
        attribute = ActiveModel::Attribute.from_database("x", value_before_type_cast, type)
        attribute.value
        attribute.forgetting_assignment
      end

      x.compare!
    end
  end
  ```

**Results**

  ```
  === big_integer ======================================================
  Warming up --------------------------------------
                before    35.830k i/100ms
                 after    99.520k i/100ms
  Calculating -------------------------------------
                before    356.010k (± 1.0%) i/s -      1.792M in   5.032655s
                 after    989.674k (± 1.2%) i/s -      4.976M in   5.028638s

  Comparison:
                 after:   989674.2 i/s
                before:   356010.4 i/s - 2.78x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    35.030k i/100ms
         after w/ read    56.005k i/100ms
  Calculating -------------------------------------
        before w/ read    349.509k (± 1.3%) i/s -      1.752M in   5.012188s
         after w/ read    558.953k (± 1.2%) i/s -      2.800M in   5.010511s

  Comparison:
         after w/ read:   558953.4 i/s
        before w/ read:   349508.6 i/s - 1.60x  (± 0.00) slower

  === boolean ==========================================================
  Warming up --------------------------------------
                before    31.989k i/100ms
                 after    99.698k i/100ms
  Calculating -------------------------------------
                before    319.969k (± 1.0%) i/s -      1.631M in   5.099207s
                 after    994.759k (± 1.1%) i/s -      4.985M in   5.011775s

  Comparison:
                 after:   994758.6 i/s
                before:   319969.3 i/s - 3.11x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    31.090k i/100ms
         after w/ read    33.306k i/100ms
  Calculating -------------------------------------
        before w/ read    308.859k (± 1.1%) i/s -      1.554M in   5.033639s
         after w/ read    332.726k (± 1.1%) i/s -      1.665M in   5.005669s

  Comparison:
         after w/ read:   332726.2 i/s
        before w/ read:   308858.6 i/s - 1.08x  (± 0.00) slower

  === date =============================================================
  Warming up --------------------------------------
                before    14.466k i/100ms
                 after    98.451k i/100ms
  Calculating -------------------------------------
                before    145.594k (± 1.4%) i/s -    737.766k in   5.068354s
                 after    990.526k (± 1.1%) i/s -      5.021M in   5.069638s

  Comparison:
                 after:   990526.4 i/s
                before:   145593.9 i/s - 6.80x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    14.130k i/100ms
         after w/ read    14.702k i/100ms
  Calculating -------------------------------------
        before w/ read    141.887k (± 1.3%) i/s -    720.630k in   5.079719s
         after w/ read    148.244k (± 1.0%) i/s -    749.802k in   5.058433s

  Comparison:
         after w/ read:   148244.1 i/s
        before w/ read:   141886.8 i/s - 1.04x  (± 0.00) slower

  === datetime =========================================================
  Warming up --------------------------------------
                before     9.484k i/100ms
                 after    97.889k i/100ms
  Calculating -------------------------------------
                before     95.640k (± 1.3%) i/s -    483.684k in   5.058190s
                 after    988.827k (± 1.1%) i/s -      4.992M in   5.049376s

  Comparison:
                 after:   988827.4 i/s
                before:    95639.7 i/s - 10.34x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read     9.440k i/100ms
         after w/ read    10.479k i/100ms
  Calculating -------------------------------------
        before w/ read     94.420k (± 1.1%) i/s -    481.440k in   5.099530s
         after w/ read    105.935k (± 1.0%) i/s -    534.429k in   5.045377s

  Comparison:
         after w/ read:   105935.2 i/s
        before w/ read:    94419.6 i/s - 1.12x  (± 0.00) slower

  === decimal ==========================================================
  Warming up --------------------------------------
                before    12.877k i/100ms
                 after    98.081k i/100ms
  Calculating -------------------------------------
                before    127.627k (± 1.4%) i/s -    643.850k in   5.045749s
                 after    990.178k (± 0.9%) i/s -      5.002M in   5.052175s

  Comparison:
                 after:   990178.0 i/s
                before:   127627.5 i/s - 7.76x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    12.640k i/100ms
         after w/ read    19.739k i/100ms
  Calculating -------------------------------------
        before w/ read    124.933k (± 1.4%) i/s -    632.000k in   5.059694s
         after w/ read    200.110k (± 1.0%) i/s -      1.007M in   5.031169s

  Comparison:
         after w/ read:   200110.3 i/s
        before w/ read:   124932.8 i/s - 1.60x  (± 0.00) slower

  === float ============================================================
  Warming up --------------------------------------
                before    45.424k i/100ms
                 after    99.512k i/100ms
  Calculating -------------------------------------
                before    449.861k (± 1.0%) i/s -      2.271M in   5.049231s
                 after    991.130k (± 1.1%) i/s -      4.976M in   5.020705s

  Comparison:
                 after:   991130.0 i/s
                before:   449861.3 i/s - 2.20x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    43.283k i/100ms
         after w/ read    42.888k i/100ms
  Calculating -------------------------------------
        before w/ read    431.630k (± 1.0%) i/s -      2.164M in   5.014423s
         after w/ read    429.631k (± 1.0%) i/s -      2.187M in   5.091609s

  Comparison:
        before w/ read:   431630.0 i/s
         after w/ read:   429631.0 i/s - same-ish: difference falls within error

  === immutable_string =================================================
  Warming up --------------------------------------
                before    36.820k i/100ms
                 after    97.812k i/100ms
  Calculating -------------------------------------
                before    369.412k (± 1.1%) i/s -      1.878M in   5.083860s
                 after    985.345k (± 1.1%) i/s -      4.988M in   5.063224s

  Comparison:
                 after:   985345.0 i/s
                before:   369411.8 i/s - 2.67x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    36.082k i/100ms
         after w/ read    37.250k i/100ms
  Calculating -------------------------------------
        before w/ read    361.134k (± 1.0%) i/s -      1.840M in   5.096085s
         after w/ read    369.471k (± 0.8%) i/s -      1.862M in   5.041349s

  Comparison:
         after w/ read:   369471.0 i/s
        before w/ read:   361133.6 i/s - 1.02x  (± 0.00) slower

  === integer ==========================================================
  Warming up --------------------------------------
                before    35.763k i/100ms
                 after    98.444k i/100ms
  Calculating -------------------------------------
                before    358.171k (± 1.1%) i/s -      1.824M in   5.092870s
                 after    995.138k (± 1.0%) i/s -      5.021M in   5.045729s

  Comparison:
                 after:   995137.6 i/s
                before:   358171.4 i/s - 2.78x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    34.534k i/100ms
         after w/ read    53.971k i/100ms
  Calculating -------------------------------------
        before w/ read    345.029k (± 1.0%) i/s -      1.727M in   5.005030s
         after w/ read    537.373k (± 0.9%) i/s -      2.699M in   5.022178s

  Comparison:
         after w/ read:   537373.4 i/s
        before w/ read:   345029.3 i/s - 1.56x  (± 0.00) slower

  === string ===========================================================
  Warming up --------------------------------------
                before    33.882k i/100ms
                 after    97.193k i/100ms
  Calculating -------------------------------------
                before    339.174k (± 1.1%) i/s -      1.728M in   5.095272s
                 after    984.544k (± 0.9%) i/s -      4.957M in   5.035057s

  Comparison:
                 after:   984543.5 i/s
                before:   339174.0 i/s - 2.90x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    32.897k i/100ms
         after w/ read    31.767k i/100ms
  Calculating -------------------------------------
        before w/ read    329.911k (± 0.8%) i/s -      1.678M in   5.085756s
         after w/ read    320.010k (± 0.6%) i/s -      1.620M in   5.062867s

  Comparison:
        before w/ read:   329910.6 i/s
         after w/ read:   320010.1 i/s - 1.03x  (± 0.00) slower

  === time =============================================================
  Warming up --------------------------------------
                before     7.461k i/100ms
                 after    97.467k i/100ms
  Calculating -------------------------------------
                before     74.854k (± 1.5%) i/s -    380.511k in   5.084475s
                 after    986.169k (± 0.9%) i/s -      4.971M in   5.040923s

  Comparison:
                 after:   986168.8 i/s
                before:    74854.5 i/s - 13.17x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read     7.438k i/100ms
         after w/ read     8.046k i/100ms
  Calculating -------------------------------------
        before w/ read     73.436k (± 1.5%) i/s -    371.900k in   5.065491s
         after w/ read     79.752k (± 1.5%) i/s -    402.300k in   5.045612s

  Comparison:
         after w/ read:    79751.7 i/s
        before w/ read:    73435.8 i/s - 1.09x  (± 0.00) slower
  ```

And, notably, this avoids potentially expensive serialize and
deserialize calls for non-basic types, such as with encrypted
attributes, when the attribute has not been read or assigned:

**Before**

  ```irb
  irb> Post.encrypts :body, encryptor: MyLoggingEncryptor.new

  irb> post = Post.create!(title: "untitled", body: "The Body")
  ! encrypting "The Body"
    TRANSACTION (0.1ms)  begin transaction
    Post Create (0.6ms)  INSERT INTO "posts" ...
  ! decrypted "The Body"
    TRANSACTION (120.2ms)  commit transaction

  irb> post.update!(title: "The Title")
    TRANSACTION (0.1ms)  begin transaction
    Post Update (0.7ms)  UPDATE "posts" ...
  ! decrypted "The Body"
  ! encrypting "The Body"
    TRANSACTION (92.1ms)  commit transaction
  ```

**After**

  ```irb
  irb> Post.encrypts :body, encryptor: MyLoggingEncryptor.new

  irb> post = Post.create!(title: "untitled", body: "The Body")
  ! encrypting "The Body"
    TRANSACTION (0.1ms)  begin transaction
    Post Create (0.7ms)  INSERT INTO "posts" ...
  ! decrypted "The Body"
    TRANSACTION (103.7ms)  commit transaction

  irb> post.update!(title: "The Title")
    TRANSACTION (0.1ms)  begin transaction
    Post Update (0.7ms)  UPDATE "posts" ...
    TRANSACTION (91.2ms)  commit transaction
  ```
2022-10-20 15:39:10 -05:00
Blake Gearin 30edef785f
Update documentation of length validation 2022-10-17 16:11:50 -05:00
Jonathan Hefner caf9413604
Merge pull request #46231 from jonathanhefner/active_model-memoize-value_for_database
Avoid unnecessary `serialize` calls after save
2022-10-16 17:13:21 -05:00
Jonathan Hefner 28ebf3c81c 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-16 16:06:16 -05:00
Jonathan Hefner 5e62c194e5 Avoid unnecessary serialize calls after save
Saving a record calls `ActiveModel::Attribute#value_for_database` on
each of its attributes.  `value_for_database`, in turn, calls
`serialize`.  After a record is successfully saved, its attributes are
reset via `ActiveModel::Attribute#forgetting_assignment`, which also
calls `value_for_database`.  This means attributes are unnecessarily
re-serialized right after they are saved.

This commit memoizes `value_for_database` so that `serialize` is not
called a 2nd time after save.  Because `value` is the single source of
truth and can change in place, the memoization carefully checks for
when `value` differs from the memoized `@value_for_database`.

This yields a small performance increase when saving, and a larger
performance increase when repeatedly reading `value_for_database` for
most types.

**Benchmark script**

  ```ruby
  # frozen_string_literal: true
  require "benchmark/ips"

  ActiveModel::Attribute.subclasses.each { |subclass| subclass.send(:public, :_value_for_database) }

  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

  attribute_set = MyModel.new(VALUES).instance_variable_get(:@attributes)

  def class_name(object) = object.class.name.demodulize

  def mimic_save(attribute)
    puts "=== #{__method__} / #{class_name attribute.type} #{class_name attribute} ".ljust(70, "=")

    Benchmark.ips do |x|
      x.report("before") do
        fresh_copy = attribute.dup
        fresh_copy._value_for_database
        fresh_copy.forgetting_assignment
      end

      x.report("after") do
        fresh_copy = attribute.dup
        fresh_copy.value_for_database
        fresh_copy.forgetting_assignment
      end

      x.compare!
    end
  end

  VALUES.each_key do |name|
    mimic_save(attribute_set[name.to_s].forgetting_assignment)
    mimic_save(attribute_set[name.to_s])
  end

  def get_value_for_database(attribute)
    puts "=== #{__method__} / #{class_name attribute.type} #{class_name attribute} ".ljust(70, "=")

    Benchmark.ips do |x|
      x.report("before") { attribute._value_for_database }
      x.report("after") { attribute.value_for_database }
      x.compare!
    end
  end

  VALUES.each_key do |name|
    get_value_for_database(attribute_set[name.to_s].forgetting_assignment)
    get_value_for_database(attribute_set[name.to_s])
  end
  ```

**`mimic_save` Results**

  ```
  === mimic_save / BigInteger FromDatabase =============================
  Warming up --------------------------------------
                before    24.460k i/100ms
                 after    28.474k i/100ms
  Calculating -------------------------------------
                before    243.390k (± 1.0%) i/s -      1.223M in   5.025334s
                 after    284.497k (± 0.8%) i/s -      1.424M in   5.004566s

  Comparison:
                 after:   284497.1 i/s
                before:   243389.7 i/s - 1.17x  (± 0.00) slower

  === mimic_save / BigInteger FromUser =================================
  Warming up --------------------------------------
                before    58.151k i/100ms
                 after    64.633k i/100ms
  Calculating -------------------------------------
                before    581.268k (± 1.2%) i/s -      2.908M in   5.002814s
                 after    645.165k (± 1.2%) i/s -      3.232M in   5.009752s

  Comparison:
                 after:   645164.8 i/s
                before:   581267.9 i/s - 1.11x  (± 0.00) slower

  === mimic_save / Boolean FromDatabase ================================
  Warming up --------------------------------------
                before    36.771k i/100ms
                 after    38.218k i/100ms
  Calculating -------------------------------------
                before    371.521k (± 1.1%) i/s -      1.875M in   5.048310s
                 after    384.021k (± 0.9%) i/s -      1.949M in   5.075966s

  Comparison:
                 after:   384021.4 i/s
                before:   371520.8 i/s - 1.03x  (± 0.00) slower

  === mimic_save / Boolean FromUser ====================================
  Warming up --------------------------------------
                before    58.738k i/100ms
                 after    63.935k i/100ms
  Calculating -------------------------------------
                before    582.358k (± 0.9%) i/s -      2.937M in   5.043559s
                 after    633.391k (± 0.9%) i/s -      3.197M in   5.047443s

  Comparison:
                 after:   633390.5 i/s
                before:   582358.2 i/s - 1.09x  (± 0.00) slower

  === mimic_save / Date FromDatabase ===================================
  Warming up --------------------------------------
                before    28.242k i/100ms
                 after    31.247k i/100ms
  Calculating -------------------------------------
                before    282.438k (± 1.0%) i/s -      1.412M in   5.000177s
                 after    311.108k (± 1.0%) i/s -      1.562M in   5.022362s

  Comparison:
                 after:   311108.4 i/s
                before:   282437.9 i/s - 1.10x  (± 0.00) slower

  === mimic_save / Date FromUser =======================================
  Warming up --------------------------------------
                before    43.427k i/100ms
                 after    47.354k i/100ms
  Calculating -------------------------------------
                before    431.978k (± 1.3%) i/s -      2.171M in   5.027373s
                 after    470.658k (± 1.3%) i/s -      2.368M in   5.031540s

  Comparison:
                 after:   470658.0 i/s
                before:   431978.2 i/s - 1.09x  (± 0.00) slower

  === mimic_save / DateTime FromDatabase ===============================
  Warming up --------------------------------------
                before    20.997k i/100ms
                 after    24.962k i/100ms
  Calculating -------------------------------------
                before    210.672k (± 0.9%) i/s -      1.071M in   5.083391s
                 after    248.114k (± 0.8%) i/s -      1.248M in   5.030687s

  Comparison:
                 after:   248114.5 i/s
                before:   210671.9 i/s - 1.18x  (± 0.00) slower

  === mimic_save / DateTime FromUser ===================================
  Warming up --------------------------------------
                before    30.406k i/100ms
                 after    45.886k i/100ms
  Calculating -------------------------------------
                before    304.374k (± 0.9%) i/s -      1.551M in   5.095184s
                 after    456.754k (± 1.3%) i/s -      2.294M in   5.023891s

  Comparison:
                 after:   456753.8 i/s
                before:   304374.0 i/s - 1.50x  (± 0.00) slower

  === mimic_save / Decimal FromDatabase ================================
  Warming up --------------------------------------
                before    11.381k i/100ms
                 after    13.632k i/100ms
  Calculating -------------------------------------
                before    112.355k (± 1.4%) i/s -    569.050k in   5.065752s
                 after    135.940k (± 1.5%) i/s -    681.600k in   5.015094s

  Comparison:
                 after:   135939.8 i/s
                before:   112355.1 i/s - 1.21x  (± 0.00) slower

  === mimic_save / Decimal FromUser ====================================
  Warming up --------------------------------------
                before    59.270k i/100ms
                 after    64.668k i/100ms
  Calculating -------------------------------------
                before    595.050k (± 1.3%) i/s -      3.023M in   5.080703s
                 after    644.206k (± 0.9%) i/s -      3.233M in   5.019581s

  Comparison:
                 after:   644205.8 i/s
                before:   595049.7 i/s - 1.08x  (± 0.00) slower

  === mimic_save / Float FromDatabase ==================================
  Warming up --------------------------------------
                before    35.564k i/100ms
                 after    35.632k i/100ms
  Calculating -------------------------------------
                before    355.836k (± 1.5%) i/s -      1.814M in   5.098367s
                 after    361.603k (± 1.1%) i/s -      1.817M in   5.026122s

  Comparison:
                 after:   361603.1 i/s
                before:   355835.7 i/s - same-ish: difference falls within error

  === mimic_save / Float FromUser ======================================
  Warming up --------------------------------------
                before    57.544k i/100ms
                 after    63.450k i/100ms
  Calculating -------------------------------------
                before    572.265k (± 1.1%) i/s -      2.877M in   5.028412s
                 after    631.023k (± 1.1%) i/s -      3.172M in   5.028143s

  Comparison:
                 after:   631022.8 i/s
                before:   572264.9 i/s - 1.10x  (± 0.00) slower

  === mimic_save / ImmutableString FromDatabase ========================
  Warming up --------------------------------------
                before    27.239k i/100ms
                 after    29.235k i/100ms
  Calculating -------------------------------------
                before    272.882k (± 1.1%) i/s -      1.389M in   5.091389s
                 after    292.142k (± 1.1%) i/s -      1.462M in   5.004132s

  Comparison:
                 after:   292142.0 i/s
                before:   272882.0 i/s - 1.07x  (± 0.00) slower

  === mimic_save / ImmutableString FromUser ============================
  Warming up --------------------------------------
                before    44.308k i/100ms
                 after    48.680k i/100ms
  Calculating -------------------------------------
                before    438.869k (± 1.2%) i/s -      2.215M in   5.048665s
                 after    482.455k (± 1.1%) i/s -      2.434M in   5.045670s

  Comparison:
                 after:   482454.7 i/s
                before:   438868.9 i/s - 1.10x  (± 0.00) slower

  === mimic_save / Integer FromDatabase ================================
  Warming up --------------------------------------
                before    25.554k i/100ms
                 after    29.236k i/100ms
  Calculating -------------------------------------
                before    254.308k (± 1.1%) i/s -      1.278M in   5.024863s
                 after    292.265k (± 1.1%) i/s -      1.462M in   5.002250s

  Comparison:
                 after:   292265.3 i/s
                before:   254308.2 i/s - 1.15x  (± 0.00) slower

  === mimic_save / Integer FromUser ====================================
  Warming up --------------------------------------
                before    46.034k i/100ms
                 after    64.028k i/100ms
  Calculating -------------------------------------
                before    458.343k (± 1.2%) i/s -      2.302M in   5.022546s
                 after    636.237k (± 1.1%) i/s -      3.201M in   5.032346s

  Comparison:
                 after:   636237.2 i/s
                before:   458343.4 i/s - 1.39x  (± 0.00) slower

  === mimic_save / String FromDatabase =================================
  Warming up --------------------------------------
                before    25.804k i/100ms
                 after    26.682k i/100ms
  Calculating -------------------------------------
                before    259.941k (± 1.2%) i/s -      1.316M in   5.063398s
                 after    268.140k (± 1.0%) i/s -      1.361M in   5.075435s

  Comparison:
                 after:   268140.2 i/s
                before:   259941.3 i/s - 1.03x  (± 0.00) slower

  === mimic_save / String FromUser =====================================
  Warming up --------------------------------------
                before    40.607k i/100ms
                 after    42.735k i/100ms
  Calculating -------------------------------------
                before    407.731k (± 1.2%) i/s -      2.071M in   5.079973s
                 after    424.659k (± 1.1%) i/s -      2.137M in   5.032247s

  Comparison:
                 after:   424659.2 i/s
                before:   407731.2 i/s - 1.04x  (± 0.00) slower

  === mimic_save / Time FromDatabase ===================================
  Warming up --------------------------------------
                before    21.555k i/100ms
                 after    25.151k i/100ms
  Calculating -------------------------------------
                before    213.479k (± 1.0%) i/s -      1.078M in   5.049047s
                 after    249.833k (± 1.2%) i/s -      1.258M in   5.034246s

  Comparison:
                 after:   249833.1 i/s
                before:   213479.1 i/s - 1.17x  (± 0.00) slower

  === mimic_save / Time FromUser =======================================
  Warming up --------------------------------------
                before    30.226k i/100ms
                 after    45.704k i/100ms
  Calculating -------------------------------------
                before    303.729k (± 1.2%) i/s -      1.542M in   5.076124s
                 after    457.186k (± 0.9%) i/s -      2.331M in   5.098810s

  Comparison:
                 after:   457186.0 i/s
                before:   303729.0 i/s - 1.51x  (± 0.00) slower
  ```

**`get_value_for_database` Results**

  ```
  === get_value_for_database / BigInteger FromDatabase =================
  Warming up --------------------------------------
                before   101.504k i/100ms
                 after   328.924k i/100ms
  Calculating -------------------------------------
                before      1.007M (± 0.7%) i/s -      5.075M in   5.040604s
                 after      3.303M (± 0.6%) i/s -     16.775M in   5.079630s

  Comparison:
                 after:  3302566.7 i/s
                before:  1006908.5 i/s - 3.28x  (± 0.00) slower

  === get_value_for_database / BigInteger FromUser =====================
  Warming up --------------------------------------
                before   282.580k i/100ms
                 after   325.867k i/100ms
  Calculating -------------------------------------
                before      2.840M (± 0.6%) i/s -     14.412M in   5.074481s
                 after      3.329M (± 0.6%) i/s -     16.945M in   5.090498s

  Comparison:
                 after:  3328905.3 i/s
                before:  2840125.6 i/s - 1.17x  (± 0.00) slower

  === get_value_for_database / Boolean FromDatabase ====================
  Warming up --------------------------------------
                before   197.974k i/100ms
                 after   327.017k i/100ms
  Calculating -------------------------------------
                before      1.984M (± 0.8%) i/s -     10.097M in   5.088429s
                 after      3.269M (± 0.7%) i/s -     16.351M in   5.001320s

  Comparison:
                 after:  3269485.0 i/s
                before:  1984376.2 i/s - 1.65x  (± 0.00) slower

  === get_value_for_database / Boolean FromUser ========================
  Warming up --------------------------------------
                before   286.138k i/100ms
                 after   340.681k i/100ms
  Calculating -------------------------------------
                before      2.900M (± 0.7%) i/s -     14.593M in   5.031863s
                 after      3.387M (± 0.6%) i/s -     17.034M in   5.028800s

  Comparison:
                 after:  3387438.6 i/s
                before:  2900285.2 i/s - 1.17x  (± 0.00) slower

  === get_value_for_database / Date FromDatabase =======================
  Warming up --------------------------------------
                before   133.983k i/100ms
                 after   327.549k i/100ms
  Calculating -------------------------------------
                before      1.344M (± 0.7%) i/s -      6.833M in   5.085972s
                 after      3.272M (± 0.7%) i/s -     16.377M in   5.005522s

  Comparison:
                 after:  3272057.0 i/s
                before:  1343591.3 i/s - 2.44x  (± 0.00) slower

  === get_value_for_database / Date FromUser ===========================
  Warming up --------------------------------------
                before   291.156k i/100ms
                 after   336.507k i/100ms
  Calculating -------------------------------------
                before      2.917M (± 1.0%) i/s -     14.849M in   5.090985s
                 after      3.383M (± 0.9%) i/s -     17.162M in   5.073857s

  Comparison:
                 after:  3382717.0 i/s
                before:  2917023.0 i/s - 1.16x  (± 0.00) slower

  === get_value_for_database / DateTime FromDatabase ===================
  Warming up --------------------------------------
                before    75.632k i/100ms
                 after   334.488k i/100ms
  Calculating -------------------------------------
                before    759.512k (± 0.8%) i/s -      3.857M in   5.078867s
                 after      3.363M (± 0.9%) i/s -     17.059M in   5.072516s

  Comparison:
                 after:  3363268.0 i/s
                before:   759512.4 i/s - 4.43x  (± 0.00) slower

  === get_value_for_database / DateTime FromUser =======================
  Warming up --------------------------------------
                before   133.780k i/100ms
                 after   330.351k i/100ms
  Calculating -------------------------------------
                before      1.346M (± 0.8%) i/s -      6.823M in   5.068844s
                 after      3.303M (± 0.9%) i/s -     16.518M in   5.001328s

  Comparison:
                 after:  3302885.8 i/s
                before:  1346115.9 i/s - 2.45x  (± 0.00) slower

  === get_value_for_database / Decimal FromDatabase ====================
  Warming up --------------------------------------
                before    43.500k i/100ms
                 after   329.669k i/100ms
  Calculating -------------------------------------
                before    437.058k (± 1.7%) i/s -      2.218M in   5.077481s
                 after      3.290M (± 0.9%) i/s -     16.483M in   5.010687s

  Comparison:
                 after:  3289905.0 i/s
                before:   437058.2 i/s - 7.53x  (± 0.00) slower

  === get_value_for_database / Decimal FromUser ========================
  Warming up --------------------------------------
                before   288.315k i/100ms
                 after   330.565k i/100ms
  Calculating -------------------------------------
                before      2.886M (± 0.7%) i/s -     14.704M in   5.095872s
                 after      3.309M (± 0.8%) i/s -     16.859M in   5.094675s

  Comparison:
                 after:  3309344.5 i/s
                before:  2885624.4 i/s - 1.15x  (± 0.00) slower

  === get_value_for_database / Float FromDatabase ======================
  Warming up --------------------------------------
                before   187.267k i/100ms
                 after   337.589k i/100ms
  Calculating -------------------------------------
                before      1.888M (± 0.9%) i/s -      9.551M in   5.057695s
                 after      3.350M (± 0.9%) i/s -     16.879M in   5.039205s

  Comparison:
                 after:  3349910.7 i/s
                before:  1888499.4 i/s - 1.77x  (± 0.00) slower

  === get_value_for_database / Float FromUser ==========================
  Warming up --------------------------------------
                before   280.405k i/100ms
                 after   338.447k i/100ms
  Calculating -------------------------------------
                before      2.822M (± 1.0%) i/s -     14.301M in   5.068052s
                 after      3.392M (± 0.8%) i/s -     17.261M in   5.089235s

  Comparison:
                 after:  3391855.1 i/s
                before:  2822015.9 i/s - 1.20x  (± 0.00) slower

  === get_value_for_database / ImmutableString FromDatabase ============
  Warming up --------------------------------------
                before   142.061k i/100ms
                 after   340.814k i/100ms
  Calculating -------------------------------------
                before      1.429M (± 0.9%) i/s -      7.245M in   5.071044s
                 after      3.369M (± 0.8%) i/s -     17.041M in   5.058261s

  Comparison:
                 after:  3369088.0 i/s
                before:  1428830.1 i/s - 2.36x  (± 0.00) slower

  === get_value_for_database / ImmutableString FromUser ================
  Warming up --------------------------------------
                before   285.588k i/100ms
                 after   338.146k i/100ms
  Calculating -------------------------------------
                before      2.890M (± 0.9%) i/s -     14.565M in   5.041037s
                 after      3.369M (± 0.9%) i/s -     16.907M in   5.018268s

  Comparison:
                 after:  3369429.6 i/s
                before:  2889532.4 i/s - 1.17x  (± 0.00) slower

  === get_value_for_database / Integer FromDatabase ====================
  Warming up --------------------------------------
                before   106.915k i/100ms
                 after   334.301k i/100ms
  Calculating -------------------------------------
                before      1.070M (± 1.0%) i/s -      5.453M in   5.098578s
                 after      3.373M (± 0.8%) i/s -     17.049M in   5.054221s

  Comparison:
                 after:  3373498.9 i/s
                before:  1069550.2 i/s - 3.15x  (± 0.00) slower

  === get_value_for_database / Integer FromUser ========================
  Warming up --------------------------------------
                before   175.719k i/100ms
                 after   339.506k i/100ms
  Calculating -------------------------------------
                before      1.760M (± 1.0%) i/s -      8.962M in   5.093648s
                 after      3.368M (± 0.8%) i/s -     16.975M in   5.040973s

  Comparison:
                 after:  3367705.1 i/s
                before:  1759569.2 i/s - 1.91x  (± 0.00) slower

  === get_value_for_database / String FromDatabase =====================
  Warming up --------------------------------------
                before   143.817k i/100ms
                 after   287.976k i/100ms
  Calculating -------------------------------------
                before      1.433M (± 0.9%) i/s -      7.191M in   5.017545s
                 after      2.898M (± 0.9%) i/s -     14.687M in   5.067782s

  Comparison:
                 after:  2898309.6 i/s
                before:  1433247.4 i/s - 2.02x  (± 0.00) slower

  === get_value_for_database / String FromUser =========================
  Warming up --------------------------------------
                before   288.320k i/100ms
                 after   287.449k i/100ms
  Calculating -------------------------------------
                before      2.891M (± 0.7%) i/s -     14.704M in   5.085633s
                 after      2.899M (± 0.6%) i/s -     14.660M in   5.057520s

  Comparison:
                 after:  2898730.4 i/s
                before:  2891484.2 i/s - same-ish: difference falls within error

  === get_value_for_database / Time FromDatabase =======================
  Warming up --------------------------------------
                before    72.976k i/100ms
                 after   335.541k i/100ms
  Calculating -------------------------------------
                before    741.313k (± 0.7%) i/s -      3.722M in   5.020770s
                 after      3.368M (± 0.6%) i/s -     17.113M in   5.080733s

  Comparison:
                 after:  3368266.8 i/s
                before:   741313.5 i/s - 4.54x  (± 0.00) slower

  === get_value_for_database / Time FromUser ===========================
  Warming up --------------------------------------
                before   137.338k i/100ms
                 after   336.559k i/100ms
  Calculating -------------------------------------
                before      1.382M (± 0.6%) i/s -      7.004M in   5.069264s
                 after      3.393M (± 0.5%) i/s -     17.165M in   5.059474s

  Comparison:
                 after:  3392622.1 i/s
                before:  1381763.3 i/s - 2.46x  (± 0.00) slower
  ```

Co-authored-by: Jorge Manrubia <jorge@hey.com>
2022-10-16 16:05:47 -05:00
Jean Boussier d917896f45 Enable verbose mode in test and report warnings as errors
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.
2022-10-11 09:25:18 +02:00
Jonathan Hefner 8e383fdad6 Avoid double type cast when serializing attributes
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
  ```
2022-09-29 11:34:29 -05:00
Aaron Patterson 5abb45d53a Dup and freeze complex types when making query attributes
This avoids problems when complex data structures are mutated _after_
being handed to ActiveRecord for processing.  For example false hits in
the query cache.

Fixes #46044
2022-09-19 09:41:39 +02:00
John Bampton 3a32915bbc Fix word case. `json` -> `JSON` 2022-09-17 04:11:36 +10:00
Jean Boussier 26826eb354
Merge pull request #45771 from andrewn617/type-cast-attribute-changed-from-and-to-options
Type cast #attribute_changed? :from and :to options
2022-09-16 12:16:27 +02:00
Yong Bakos cfca0bfab5 ActiveModel: Fix method name in Callbacks documentation.
:initializer -> :initialize
2022-09-12 11:45:54 -07:00
Fabio Sangiovanni 12f3da0ac3 Fix punctuation in `has_secure_password` docs. 2022-08-30 19:32:00 +02:00
Jacopo 2f5136a3be Normalize virtual attributes on `ActiveRecord::Persistence#becomes`
When source and target classes have a different set of attributes adapts
attributes such that the extra attributes from target are added.

Fixes #41195

Co-authored-by: SampsonCrowley <sampsonsprojects@gmail.com>
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
2022-08-24 22:51:57 +02:00
Andrew Novoselac 7519457678 Fixes a defect in ActiveModel::Dirty#attribute_changed? where the :from
and :to options are not being type cast. For example, for an enum
attribute, attribute_changed? should handle a String, Symbol or Integer
for the :from and :to options.
2022-08-05 11:52:20 -04:00
Jonathan Hefner 239d6fa67d Use tap in conditionally-required password example [ci-skip] 2022-08-04 11:39:29 -05:00
Jonathan Hefner f53e3ad938 Add example of conditionally requiring a password [ci-skip]
This example addresses the use case brought up by #45487, which has been
reverted by #45753.
2022-08-03 16:14:08 -05:00
Jonathan Hefner 6020f5331d Revert "Allow passing hash on secure password validations"
This reverts #45487 (8804128ad4) until a
better API can be decided upon.
2022-08-03 11:52:30 -05:00
Kevin Jacoby 8804128ad4 Allow passing hash on secure password validations
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.
2022-07-26 10:11:43 -05:00
fatkodima 3822172c00 Fix caching of missed translations 2022-07-15 14:49:11 -05:00
Gannon McGibbon 17b4b8fd63 Revert "Provide pattern matching for ActiveModel"
This reverts commit 7e499b25ac.
2022-07-09 00:13:23 -04:00
Jean Boussier 01f0cea3b5 Fix code cache namespacing for proxied attribute methods
Fix: https://github.com/rails/rails/issues/45416

The suffix need to be included in the namespace as it is used to
generate the code.
2022-06-25 09:02:41 +02:00
Shouichi Kamiya f2695d2dc0 Use const_get to get validator classes
Though we needed to use constantize for ruby < 2.0, ruby >= 2.0
const_get can handle namespace.

> https://docs.ruby-lang.org/en/2.0.0/NEWS.html
> Module#const_get accepts a qualified constant string, e.g. Object.const_get(“Foo::Bar::Baz”)
2022-06-01 17:14:45 +09:00
Jonathan Hefner 0fb166e81c Improve performance for contextual validations
This change reduces retained memory when registering contextual
validations, and reduces overhead when running contextual validations.

Benchmark script:

```ruby
require "benchmark/memory"
require "benchmark/ips"

class Post
  include ActiveModel::Validations

  def foo; end
  def bar; end
end

class Comment
  include ActiveModel::Validations

  def foo; end
  def bar; end
end

Benchmark.memory do |x|
  x.report("warmup") do
    Post.validate :will_not_be_called, on: :warmup
    Comment.validate :will_not_be_called, on: :warmup
  end

  x.report("register validations") do
    Post.validate :foo, on: :create
    Post.validate :bar, on: :update
    Comment.validate :foo, on: :create
    Comment.validate :bar, on: :update
  end
end

post = Post.new

Benchmark.ips do |x|
  x.report("run validations") do
    post.valid?(:create)
  end
end
```

Before:

```
Calculating -------------------------------------
              warmup     3.320k memsize (     1.552k retained)
                        48.000  objects (    21.000  retained)
                         0.000  strings (     0.000  retained)
register validations     5.856k memsize (     2.456k retained)
                        92.000  objects (    33.000  retained)
                         0.000  strings (     0.000  retained)
Warming up --------------------------------------
     run validations     6.232k i/100ms
Calculating -------------------------------------
     run validations     61.826k (± 0.7%) i/s -    311.600k in   5.040176s
```

After:

```
Calculating -------------------------------------
              warmup     3.960k memsize (     1.400k retained)
                        51.000  objects (    17.000  retained)
                         0.000  strings (     0.000  retained)
register validations     6.368k memsize (     1.384k retained)
                        94.000  objects (    21.000  retained)
                         0.000  strings (     0.000  retained)
Warming up --------------------------------------
     run validations     6.998k i/100ms
Calculating -------------------------------------
     run validations     70.741k (± 0.6%) i/s -    356.898k in   5.045347s
```
2022-05-27 15:37:11 -05:00
Jonathan Hefner 5389c56292 Dup options in validates_with
Some validators, such as validators that inherit from `EachValidator`,
mutate the options they receive.  This can cause problems when passing
multiple validators and options to `validates_with`.  This can also be a
problem if a validator deletes standard options such as `:if` and `:on`,
because the validation callback would then not receive them.

This commit modifies `validates_with` to `dup` options before passing
them to validators, thus preventing these issues.

Fixes #44460.
Closes #44476.

Co-authored-by: Dieter Späth <dieter.spaeth@lanes-planes.com>
2022-05-26 15:08:00 -05:00
fatkodima 91cd5cbc93 Support infinite ranges for `LengthValidator`s `:in`/`:within` options 2022-05-20 20:41:18 +03:00
Bo Jeanes 28e40a6405 Add beginless range support to clusivity 2022-05-20 07:46:01 +10:00