rails/activemodel
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
..
bin Use frozen string literal in activemodel/ 2017-07-16 20:11:16 +03:00
lib Avoid double type cast when serializing attributes 2022-09-29 11:34:29 -05:00
test Avoid double type cast when serializing attributes 2022-09-29 11:34:29 -05:00
activemodel.gemspec Fix gemspec 2021-11-15 21:06:21 +00:00
CHANGELOG.md Revert "Allow passing hash on secure password validations" 2022-08-03 11:52:30 -05:00
MIT-LICENSE Bump license years to 2022 [ci-skip] 2022-01-01 15:22:15 +09:00
Rakefile Load framework test files in deterministic order 2019-12-16 16:55:06 +00:00
README.rdoc Introduce ActiveModel::API 2021-09-15 18:24:47 +02:00

= Active Model -- model interfaces for Rails

Active Model provides a known set of interfaces for usage in model classes.
They allow for Action Pack helpers to interact with non-Active Record models,
for example. Active Model also helps with building custom ORMs for use outside of
the Rails framework.

You can read more about Active Model in the {Active Model Basics}[https://edgeguides.rubyonrails.org/active_model_basics.html] guide.

Prior to Rails 3.0, if a plugin or gem developer wanted to have an object
interact with Action Pack helpers, it was required to either copy chunks of
code from Rails, or monkey patch entire helpers to make them handle objects
that did not exactly conform to the Active Record interface. This would result
in code duplication and fragile applications that broke on upgrades. Active
Model solves this by defining an explicit API. You can read more about the
API in <tt>ActiveModel::Lint::Tests</tt>.

Active Model provides a default module that implements the basic API required
to integrate with Action Pack out of the box: <tt>ActiveModel::API</tt>.

    class Person
      include ActiveModel::API

      attr_accessor :name, :age
      validates_presence_of :name
    end

    person = Person.new(name: 'bob', age: '18')
    person.name   # => 'bob'
    person.age    # => '18'
    person.valid? # => true

It includes model name introspections, conversions, translations and
validations, resulting in a class suitable to be used with Action Pack.
See <tt>ActiveModel::API</tt> for more examples.

Active Model also provides the following functionality to have ORM-like
behavior out of the box:

* Add attribute magic to objects

    class Person
      include ActiveModel::AttributeMethods

      attribute_method_prefix 'clear_'
      define_attribute_methods :name, :age

      attr_accessor :name, :age

      def clear_attribute(attr)
        send("#{attr}=", nil)
      end
    end

    person = Person.new
    person.clear_name
    person.clear_age

  {Learn more}[link:classes/ActiveModel/AttributeMethods.html]

* Callbacks for certain operations

    class Person
      extend ActiveModel::Callbacks
      define_model_callbacks :create

      def create
        run_callbacks :create do
          # Your create action methods here
        end
      end
    end

  This generates +before_create+, +around_create+ and +after_create+
  class methods that wrap your create method.

  {Learn more}[link:classes/ActiveModel/Callbacks.html]

* Tracking value changes

    class Person
      include ActiveModel::Dirty

      define_attribute_methods :name

      def name
        @name
      end

      def name=(val)
        name_will_change! unless val == @name
        @name = val
      end

      def save
        # do persistence work
        changes_applied
      end
    end

    person = Person.new
    person.name             # => nil
    person.changed?         # => false
    person.name = 'bob'
    person.changed?         # => true
    person.changed          # => ['name']
    person.changes          # => { 'name' => [nil, 'bob'] }
    person.save
    person.name = 'robert'
    person.save
    person.previous_changes # => {'name' => ['bob, 'robert']}

  {Learn more}[link:classes/ActiveModel/Dirty.html]

* Adding +errors+ interface to objects

  Exposing error messages allows objects to interact with Action Pack
  helpers seamlessly.

    class Person

      def initialize
        @errors = ActiveModel::Errors.new(self)
      end

      attr_accessor :name
      attr_reader   :errors

      def validate!
        errors.add(:name, "cannot be nil") if name.nil?
      end

      def self.human_attribute_name(attr, options = {})
        "Name"
      end
    end

    person = Person.new
    person.name = nil
    person.validate!
    person.errors.full_messages
    # => ["Name cannot be nil"]

  {Learn more}[link:classes/ActiveModel/Errors.html]

* Model name introspection

    class NamedPerson
      extend ActiveModel::Naming
    end

    NamedPerson.model_name.name   # => "NamedPerson"
    NamedPerson.model_name.human  # => "Named person"

  {Learn more}[link:classes/ActiveModel/Naming.html]

* Making objects serializable

  <tt>ActiveModel::Serialization</tt> provides a standard interface for your object
  to provide +to_json+ serialization.

    class SerialPerson
      include ActiveModel::Serialization

      attr_accessor :name

      def attributes
        {'name' => name}
      end
    end

    s = SerialPerson.new
    s.serializable_hash   # => {"name"=>nil}

    class SerialPerson
      include ActiveModel::Serializers::JSON
    end

    s = SerialPerson.new
    s.to_json             # => "{\"name\":null}"

  {Learn more}[link:classes/ActiveModel/Serialization.html]

* Internationalization (i18n) support

    class Person
      extend ActiveModel::Translation
    end

    Person.human_attribute_name('my_attribute')
    # => "My attribute"

  {Learn more}[link:classes/ActiveModel/Translation.html]

* Validation support

    class Person
      include ActiveModel::Validations

      attr_accessor :first_name, :last_name

      validates_each :first_name, :last_name do |record, attr, value|
        record.errors.add attr, "starts with z." if value.start_with?("z")
      end
    end

    person = Person.new
    person.first_name = 'zoolander'
    person.valid?  # => false

  {Learn more}[link:classes/ActiveModel/Validations.html]

* Custom validators

    class HasNameValidator < ActiveModel::Validator
      def validate(record)
        record.errors.add(:name, "must exist") if record.name.blank?
      end
    end

    class ValidatorPerson
      include ActiveModel::Validations
      validates_with HasNameValidator
      attr_accessor :name
    end

    p = ValidatorPerson.new
    p.valid?                  # =>  false
    p.errors.full_messages    # => ["Name must exist"]
    p.name = "Bob"
    p.valid?                  # =>  true

  {Learn more}[link:classes/ActiveModel/Validator.html]


== Download and installation

The latest version of Active Model can be installed with RubyGems:

  $ gem install activemodel

Source code can be downloaded as part of the Rails project on GitHub

* https://github.com/rails/rails/tree/main/activemodel


== License

Active Model is released under the MIT license:

* https://opensource.org/licenses/MIT


== Support

API documentation is at:

* https://api.rubyonrails.org

Bug reports for the Ruby on Rails project can be filed here:

* https://github.com/rails/rails/issues

Feature requests should be discussed on the rails-core mailing list here:

* https://discuss.rubyonrails.org/c/rubyonrails-core