diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 253ec94bf9c91..ee3ee809d1408 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,3 +1,24 @@
+* Port the `BeforeTypeCast` module to Active Model. Classes that include
+ `ActiveModel::Attributes` will now automatically define methods such as
+ `*_before_type_cast`, `*_for_database`, etc. These methods behave the same
+ for Active Model as they do for Active Record.
+
+ ```ruby
+ class MyModel
+ include ActiveModel::Attributes
+
+ attribute :my_attribute, :integer
+ end
+
+ m = MyModel.new
+ m.my_attribute = "123"
+ m.my_attribute # => 123
+ m.my_attribute_before_type_cast # => "123"
+ m.read_attribute_before_type_cast(:my_attribute) # => "123"
+ ```
+
+ *Jonathan Hefner*
+
* Port the `type_for_attribute` method to Active Model. Classes that include
`ActiveModel::Attributes` will now provide this method. This method behaves
the same for Active Model as it does for Active Record.
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index cda8f412d8c35..049704f6a9ab7 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -39,6 +39,7 @@ module ActiveModel
autoload :AttributeAssignment
autoload :AttributeMethods
autoload :AttributeRegistration
+ autoload :BeforeTypeCast
autoload :BlockValidator, "active_model/validator"
autoload :Callbacks
autoload :Conversion
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index b9582566cdd22..cf64937f08b14 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -376,6 +376,10 @@ def aliases_by_attribute_name # :nodoc:
@aliases_by_attribute_name ||= Hash.new { |h, k| h[k] = [] }
end
+ def resolve_attribute_name(name) # :nodoc:
+ attribute_aliases.fetch(super, &:itself)
+ end
+
private
def inherited(base) # :nodoc:
super
@@ -384,10 +388,6 @@ def inherited(base) # :nodoc:
end
end
- def resolve_attribute_name(name)
- attribute_aliases.fetch(super, &:itself)
- end
-
def generated_attribute_methods
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
end
diff --git a/activemodel/lib/active_model/attribute_registration.rb b/activemodel/lib/active_model/attribute_registration.rb
index 6574c01c9beaa..420d86e1ba973 100644
--- a/activemodel/lib/active_model/attribute_registration.rb
+++ b/activemodel/lib/active_model/attribute_registration.rb
@@ -55,6 +55,10 @@ def type_for_attribute(attribute_name, &block)
end
end
+ def resolve_attribute_name(name) # :nodoc:
+ name.to_s
+ end
+
private
PendingType = Struct.new(:name, :type) do # :nodoc:
def apply_to(attribute_set)
@@ -103,10 +107,6 @@ def reset_default_attributes!
@attribute_types = nil
end
- def resolve_attribute_name(name)
- name.to_s
- end
-
def resolve_type_name(name, **options)
Type.lookup(name, **options)
end
diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb
index 0c21b57201d03..d8a123dffee7e 100644
--- a/activemodel/lib/active_model/attributes.rb
+++ b/activemodel/lib/active_model/attributes.rb
@@ -31,6 +31,7 @@ module Attributes
extend ActiveSupport::Concern
include ActiveModel::AttributeRegistration
include ActiveModel::AttributeMethods
+ include ActiveModel::BeforeTypeCast
included do
attribute_method_suffix "=", parameters: "value"
diff --git a/activemodel/lib/active_model/before_type_cast.rb b/activemodel/lib/active_model/before_type_cast.rb
new file mode 100644
index 0000000000000..cb64c71037691
--- /dev/null
+++ b/activemodel/lib/active_model/before_type_cast.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ # This module provides a way to read the value of attributes before type
+ # casting and deserialization. It uses ActiveModel::AttributeMethods to define
+ # attribute methods with the following suffixes:
+ #
+ # * +_before_type_cast+
+ # * +_for_database+
+ # * +_came_from_user?+
+ #
+ # ==== Examples
+ #
+ # class Task
+ # include ActiveModel::Attributes
+ #
+ # attribute :completed_at, :datetime
+ # end
+ #
+ # task = Task.new
+ # task.completed_at # => nil
+ # task.completed_at_before_type_cast # => nil
+ # task.completed_at_for_database # => nil
+ # task.completed_at_came_from_user? # => false
+ #
+ # task.completed_at = "1999-12-31T23:59:59-0500"
+ # task.completed_at # => 1999-12-31 23:59:59 -0500
+ # task.completed_at_before_type_cast # => "1999-12-31T23:59:59-0500"
+ # task.completed_at_for_database # => 2000-01-01 04:59:59 UTC
+ # task.completed_at_came_from_user? # => true
+ #
+ module BeforeTypeCast
+ extend ActiveSupport::Concern
+
+ included do
+ attribute_method_suffix "_before_type_cast", "_for_database", "_came_from_user?", parameters: false
+ end
+
+ # Returns the value of the specified attribute before type casting and
+ # deserialization.
+ #
+ # class Task
+ # include ActiveModel::Attributes
+ #
+ # attribute :completed_at, :datetime
+ # end
+ #
+ # task = Task.new
+ # task.completed_at = "1999-12-31T23:59:59-0500"
+ #
+ # task.completed_at # => 1999-12-31 23:59:59 -0500
+ # task.read_attribute_before_type_cast("completed_at") # => "1999-12-31T23:59:59-0500"
+ #
+ def read_attribute_before_type_cast(attribute_name)
+ attribute_before_type_cast(self.class.resolve_attribute_name(attribute_name))
+ end
+
+ # Returns the value of the specified attribute after serialization.
+ #
+ # class Task
+ # include ActiveModel::Attributes
+ #
+ # attribute :completed_at, :datetime
+ # end
+ #
+ # task = Task.new
+ # task.completed_at = "1999-12-31T23:59:59-0500"
+ #
+ # task.completed_at # => 1999-12-31 23:59:59 -0500
+ # task.read_attribute_for_database("completed_at") # => 2000-01-01 04:59:59 UTC
+ #
+ def read_attribute_for_database(attribute_name)
+ attribute_for_database(self.class.resolve_attribute_name(attribute_name))
+ end
+
+ # Returns a Hash of attributes before type casting and deserialization.
+ #
+ # class Task
+ # include ActiveModel::Attributes
+ #
+ # attribute :completed_at, :datetime
+ # end
+ #
+ # task = Task.new
+ # task.completed_at = "1999-12-31T23:59:59-0500"
+ #
+ # task.attributes # => {"completed_at"=>1999-12-31 23:59:59 -0500}
+ # task.attributes_before_type_cast # => {"completed_at"=>"1999-12-31T23:59:59-0500"}
+ #
+ def attributes_before_type_cast
+ @attributes.values_before_type_cast
+ end
+
+ # Returns a Hash of attributes for persisting.
+ #
+ # class Task
+ # include ActiveModel::Attributes
+ #
+ # attribute :completed_at, :datetime
+ # end
+ #
+ # task = Task.new
+ # task.completed_at = "1999-12-31T23:59:59-0500"
+ #
+ # task.attributes # => {"completed_at"=>1999-12-31 23:59:59 -0500}
+ # task.attributes_for_database # => {"completed_at"=>2000-01-01 04:59:59 UTC}
+ #
+ def attributes_for_database
+ @attributes.values_for_database
+ end
+
+ private
+ # Dispatch target for *_before_type_cast attribute methods.
+ def attribute_before_type_cast(attr_name)
+ @attributes[attr_name].value_before_type_cast
+ end
+
+ # Dispatch target for *_for_database attribute methods.
+ def attribute_for_database(attr_name)
+ @attributes[attr_name].value_for_database
+ end
+
+ # Dispatch target for *_came_from_user? attribute methods.
+ def attribute_came_from_user?(attr_name)
+ @attributes[attr_name].came_from_user?
+ end
+ end
+end
diff --git a/activemodel/test/cases/before_type_cast_test.rb b/activemodel/test/cases/before_type_cast_test.rb
new file mode 100644
index 0000000000000..ecdcc6be1e26a
--- /dev/null
+++ b/activemodel/test/cases/before_type_cast_test.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+class BeforeTypeCastTest < ActiveModel::TestCase
+ class Developer
+ include ActiveModel::Attributes
+
+ attribute :name, :string
+ attribute :salary, :integer
+ attribute :active, :boolean
+ alias_attribute :compensation, :salary
+
+ def initialize(attributes = {})
+ super()
+ attributes.each { |name, value| public_send("#{name}=", value) }
+ end
+ end
+
+ setup do
+ @before_type_cast = { name: 1234, salary: "56789", active: "0" }
+ @after_type_cast = { name: "1234", salary: 56789, active: false }
+ @developer = Developer.new(@before_type_cast)
+ end
+
+ test "#read_attribute_before_type_cast" do
+ assert_equal @before_type_cast[:salary], @developer.read_attribute_before_type_cast(:salary)
+ end
+
+ test "#read_attribute_before_type_cast with aliased attribute" do
+ assert_equal @before_type_cast[:salary], @developer.read_attribute_before_type_cast(:compensation)
+ end
+
+ test "#read_attribute_for_database" do
+ assert_equal @after_type_cast[:salary], @developer.read_attribute_for_database(:salary)
+ end
+
+ test "#read_attribute_for_database with aliased attribute" do
+ assert_equal @after_type_cast[:salary], @developer.read_attribute_for_database(:compensation)
+ end
+
+ test "#attributes_before_type_cast" do
+ assert_equal @before_type_cast.transform_keys(&:to_s), @developer.attributes_before_type_cast
+ end
+
+ test "#attributes_before_type_cast with missing attributes" do
+ assert_equal @before_type_cast.to_h { |key, value| [key.to_s, nil] }, Developer.new.attributes_before_type_cast
+ end
+
+ test "#attributes_for_database" do
+ assert_equal @after_type_cast.transform_keys(&:to_s), @developer.attributes_for_database
+ end
+
+ test "#*_before_type_cast" do
+ assert_equal @before_type_cast[:salary], @developer.salary_before_type_cast
+ end
+
+ test "#*_before_type_cast with aliased attribute" do
+ assert_equal @before_type_cast[:salary], @developer.compensation_before_type_cast
+ end
+
+ test "#*_for_database" do
+ assert_equal @after_type_cast[:salary], @developer.salary_for_database
+ end
+
+ test "#*_for_database with aliased attribute" do
+ assert_equal @after_type_cast[:salary], @developer.compensation_for_database
+ end
+
+ test "#*_came_from_user?" do
+ assert_predicate @developer, :salary_came_from_user?
+ assert_not_predicate Developer.new, :salary_came_from_user?
+ end
+
+ test "#*_came_from_user? with aliased attribute" do
+ assert_predicate @developer, :compensation_came_from_user?
+ assert_not_predicate Developer.new, :compensation_came_from_user?
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
index e4d562ad62276..b16b0c0e87d72 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -2,105 +2,10 @@
module ActiveRecord
module AttributeMethods
- # = Active Record Attribute Methods Before Type Cast
- #
- # ActiveRecord::AttributeMethods::BeforeTypeCast provides a way to
- # read the value of the attributes before typecasting and deserialization.
- #
- # class Task < ActiveRecord::Base
- # end
- #
- # task = Task.new(id: '1', completed_on: '2012-10-21')
- # task.id # => 1
- # task.completed_on # => Sun, 21 Oct 2012
- #
- # task.attributes_before_type_cast
- # # => {"id"=>"1", "completed_on"=>"2012-10-21", ... }
- # task.read_attribute_before_type_cast('id') # => "1"
- # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
- #
- # In addition to #read_attribute_before_type_cast and #attributes_before_type_cast,
- # it declares a method for all attributes with the *_before_type_cast
- # suffix.
- #
- # task.id_before_type_cast # => "1"
- # task.completed_on_before_type_cast # => "2012-10-21"
+ # See ActiveModel::BeforeTypeCast.
module BeforeTypeCast
extend ActiveSupport::Concern
-
- included do
- attribute_method_suffix "_before_type_cast", "_for_database", parameters: false
- attribute_method_suffix "_came_from_user?", parameters: false
- end
-
- # Returns the value of the attribute identified by +attr_name+ before
- # typecasting and deserialization.
- #
- # class Task < ActiveRecord::Base
- # end
- #
- # task = Task.new(id: '1', completed_on: '2012-10-21')
- # task.read_attribute('id') # => 1
- # task.read_attribute_before_type_cast('id') # => '1'
- # task.read_attribute('completed_on') # => Sun, 21 Oct 2012
- # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
- # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21"
- def read_attribute_before_type_cast(attr_name)
- name = attr_name.to_s
- name = self.class.attribute_aliases[name] || name
-
- attribute_before_type_cast(name)
- end
-
- # Returns the value of the attribute identified by +attr_name+ after
- # serialization.
- #
- # class Book < ActiveRecord::Base
- # enum status: { draft: 1, published: 2 }
- # end
- #
- # book = Book.new(status: "published")
- # book.read_attribute(:status) # => "published"
- # book.read_attribute_for_database(:status) # => 2
- def read_attribute_for_database(attr_name)
- name = attr_name.to_s
- name = self.class.attribute_aliases[name] || name
-
- attribute_for_database(name)
- end
-
- # Returns a hash of attributes before typecasting and deserialization.
- #
- # class Task < ActiveRecord::Base
- # end
- #
- # task = Task.new(title: nil, is_done: true, completed_on: '2012-10-21')
- # task.attributes
- # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>Sun, 21 Oct 2012, "created_at"=>nil, "updated_at"=>nil}
- # task.attributes_before_type_cast
- # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
- def attributes_before_type_cast
- @attributes.values_before_type_cast
- end
-
- # Returns a hash of attributes for assignment to the database.
- def attributes_for_database
- @attributes.values_for_database
- end
-
- private
- # Dispatch target for *_before_type_cast attribute methods.
- def attribute_before_type_cast(attr_name)
- @attributes[attr_name].value_before_type_cast
- end
-
- def attribute_for_database(attr_name)
- @attributes[attr_name].value_for_database
- end
-
- def attribute_came_from_user?(attr_name)
- @attributes[attr_name].came_from_user?
- end
+ include ActiveModel::BeforeTypeCast
end
end
end