Skip to content

Commit e400b8a

Browse files
Merge pull request rails#44534 from jonathanhefner/active_model-before_type_cast
Port `BeforeTypeCast` to Active Model
2 parents 7d525d3 + cfb72c9 commit e400b8a

File tree

8 files changed

+240
-105
lines changed

8 files changed

+240
-105
lines changed

activemodel/CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
* Port the `BeforeTypeCast` module to Active Model. Classes that include
2+
`ActiveModel::Attributes` will now automatically define methods such as
3+
`*_before_type_cast`, `*_for_database`, etc. These methods behave the same
4+
for Active Model as they do for Active Record.
5+
6+
```ruby
7+
class MyModel
8+
include ActiveModel::Attributes
9+
10+
attribute :my_attribute, :integer
11+
end
12+
13+
m = MyModel.new
14+
m.my_attribute = "123"
15+
m.my_attribute # => 123
16+
m.my_attribute_before_type_cast # => "123"
17+
m.read_attribute_before_type_cast(:my_attribute) # => "123"
18+
```
19+
20+
*Jonathan Hefner*
21+
122
* Port the `type_for_attribute` method to Active Model. Classes that include
223
`ActiveModel::Attributes` will now provide this method. This method behaves
324
the same for Active Model as it does for Active Record.

activemodel/lib/active_model.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ module ActiveModel
3939
autoload :AttributeAssignment
4040
autoload :AttributeMethods
4141
autoload :AttributeRegistration
42+
autoload :BeforeTypeCast
4243
autoload :BlockValidator, "active_model/validator"
4344
autoload :Callbacks
4445
autoload :Conversion

activemodel/lib/active_model/attribute_methods.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,10 @@ def aliases_by_attribute_name # :nodoc:
376376
@aliases_by_attribute_name ||= Hash.new { |h, k| h[k] = [] }
377377
end
378378

379+
def resolve_attribute_name(name) # :nodoc:
380+
attribute_aliases.fetch(super, &:itself)
381+
end
382+
379383
private
380384
def inherited(base) # :nodoc:
381385
super
@@ -384,10 +388,6 @@ def inherited(base) # :nodoc:
384388
end
385389
end
386390

387-
def resolve_attribute_name(name)
388-
attribute_aliases.fetch(super, &:itself)
389-
end
390-
391391
def generated_attribute_methods
392392
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
393393
end

activemodel/lib/active_model/attribute_registration.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ def type_for_attribute(attribute_name, &block)
5555
end
5656
end
5757

58+
def resolve_attribute_name(name) # :nodoc:
59+
name.to_s
60+
end
61+
5862
private
5963
PendingType = Struct.new(:name, :type) do # :nodoc:
6064
def apply_to(attribute_set)
@@ -103,10 +107,6 @@ def reset_default_attributes!
103107
@attribute_types = nil
104108
end
105109

106-
def resolve_attribute_name(name)
107-
name.to_s
108-
end
109-
110110
def resolve_type_name(name, **options)
111111
Type.lookup(name, **options)
112112
end

activemodel/lib/active_model/attributes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ module Attributes
3131
extend ActiveSupport::Concern
3232
include ActiveModel::AttributeRegistration
3333
include ActiveModel::AttributeMethods
34+
include ActiveModel::BeforeTypeCast
3435

3536
included do
3637
attribute_method_suffix "=", parameters: "value"
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveModel
4+
# This module provides a way to read the value of attributes before type
5+
# casting and deserialization. It uses ActiveModel::AttributeMethods to define
6+
# attribute methods with the following suffixes:
7+
#
8+
# * +_before_type_cast+
9+
# * +_for_database+
10+
# * +_came_from_user?+
11+
#
12+
# ==== Examples
13+
#
14+
# class Task
15+
# include ActiveModel::Attributes
16+
#
17+
# attribute :completed_at, :datetime
18+
# end
19+
#
20+
# task = Task.new
21+
# task.completed_at # => nil
22+
# task.completed_at_before_type_cast # => nil
23+
# task.completed_at_for_database # => nil
24+
# task.completed_at_came_from_user? # => false
25+
#
26+
# task.completed_at = "1999-12-31T23:59:59-0500"
27+
# task.completed_at # => 1999-12-31 23:59:59 -0500
28+
# task.completed_at_before_type_cast # => "1999-12-31T23:59:59-0500"
29+
# task.completed_at_for_database # => 2000-01-01 04:59:59 UTC
30+
# task.completed_at_came_from_user? # => true
31+
#
32+
module BeforeTypeCast
33+
extend ActiveSupport::Concern
34+
35+
included do
36+
attribute_method_suffix "_before_type_cast", "_for_database", "_came_from_user?", parameters: false
37+
end
38+
39+
# Returns the value of the specified attribute before type casting and
40+
# deserialization.
41+
#
42+
# class Task
43+
# include ActiveModel::Attributes
44+
#
45+
# attribute :completed_at, :datetime
46+
# end
47+
#
48+
# task = Task.new
49+
# task.completed_at = "1999-12-31T23:59:59-0500"
50+
#
51+
# task.completed_at # => 1999-12-31 23:59:59 -0500
52+
# task.read_attribute_before_type_cast("completed_at") # => "1999-12-31T23:59:59-0500"
53+
#
54+
def read_attribute_before_type_cast(attribute_name)
55+
attribute_before_type_cast(self.class.resolve_attribute_name(attribute_name))
56+
end
57+
58+
# Returns the value of the specified attribute after serialization.
59+
#
60+
# class Task
61+
# include ActiveModel::Attributes
62+
#
63+
# attribute :completed_at, :datetime
64+
# end
65+
#
66+
# task = Task.new
67+
# task.completed_at = "1999-12-31T23:59:59-0500"
68+
#
69+
# task.completed_at # => 1999-12-31 23:59:59 -0500
70+
# task.read_attribute_for_database("completed_at") # => 2000-01-01 04:59:59 UTC
71+
#
72+
def read_attribute_for_database(attribute_name)
73+
attribute_for_database(self.class.resolve_attribute_name(attribute_name))
74+
end
75+
76+
# Returns a Hash of attributes before type casting and deserialization.
77+
#
78+
# class Task
79+
# include ActiveModel::Attributes
80+
#
81+
# attribute :completed_at, :datetime
82+
# end
83+
#
84+
# task = Task.new
85+
# task.completed_at = "1999-12-31T23:59:59-0500"
86+
#
87+
# task.attributes # => {"completed_at"=>1999-12-31 23:59:59 -0500}
88+
# task.attributes_before_type_cast # => {"completed_at"=>"1999-12-31T23:59:59-0500"}
89+
#
90+
def attributes_before_type_cast
91+
@attributes.values_before_type_cast
92+
end
93+
94+
# Returns a Hash of attributes for persisting.
95+
#
96+
# class Task
97+
# include ActiveModel::Attributes
98+
#
99+
# attribute :completed_at, :datetime
100+
# end
101+
#
102+
# task = Task.new
103+
# task.completed_at = "1999-12-31T23:59:59-0500"
104+
#
105+
# task.attributes # => {"completed_at"=>1999-12-31 23:59:59 -0500}
106+
# task.attributes_for_database # => {"completed_at"=>2000-01-01 04:59:59 UTC}
107+
#
108+
def attributes_for_database
109+
@attributes.values_for_database
110+
end
111+
112+
private
113+
# Dispatch target for <tt>*_before_type_cast</tt> attribute methods.
114+
def attribute_before_type_cast(attr_name)
115+
@attributes[attr_name].value_before_type_cast
116+
end
117+
118+
# Dispatch target for <tt>*_for_database</tt> attribute methods.
119+
def attribute_for_database(attr_name)
120+
@attributes[attr_name].value_for_database
121+
end
122+
123+
# Dispatch target for <tt>*_came_from_user?</tt> attribute methods.
124+
def attribute_came_from_user?(attr_name)
125+
@attributes[attr_name].came_from_user?
126+
end
127+
end
128+
end
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# frozen_string_literal: true
2+
3+
require "cases/helper"
4+
5+
class BeforeTypeCastTest < ActiveModel::TestCase
6+
class Developer
7+
include ActiveModel::Attributes
8+
9+
attribute :name, :string
10+
attribute :salary, :integer
11+
attribute :active, :boolean
12+
alias_attribute :compensation, :salary
13+
14+
def initialize(attributes = {})
15+
super()
16+
attributes.each { |name, value| public_send("#{name}=", value) }
17+
end
18+
end
19+
20+
setup do
21+
@before_type_cast = { name: 1234, salary: "56789", active: "0" }
22+
@after_type_cast = { name: "1234", salary: 56789, active: false }
23+
@developer = Developer.new(@before_type_cast)
24+
end
25+
26+
test "#read_attribute_before_type_cast" do
27+
assert_equal @before_type_cast[:salary], @developer.read_attribute_before_type_cast(:salary)
28+
end
29+
30+
test "#read_attribute_before_type_cast with aliased attribute" do
31+
assert_equal @before_type_cast[:salary], @developer.read_attribute_before_type_cast(:compensation)
32+
end
33+
34+
test "#read_attribute_for_database" do
35+
assert_equal @after_type_cast[:salary], @developer.read_attribute_for_database(:salary)
36+
end
37+
38+
test "#read_attribute_for_database with aliased attribute" do
39+
assert_equal @after_type_cast[:salary], @developer.read_attribute_for_database(:compensation)
40+
end
41+
42+
test "#attributes_before_type_cast" do
43+
assert_equal @before_type_cast.transform_keys(&:to_s), @developer.attributes_before_type_cast
44+
end
45+
46+
test "#attributes_before_type_cast with missing attributes" do
47+
assert_equal @before_type_cast.to_h { |key, value| [key.to_s, nil] }, Developer.new.attributes_before_type_cast
48+
end
49+
50+
test "#attributes_for_database" do
51+
assert_equal @after_type_cast.transform_keys(&:to_s), @developer.attributes_for_database
52+
end
53+
54+
test "#*_before_type_cast" do
55+
assert_equal @before_type_cast[:salary], @developer.salary_before_type_cast
56+
end
57+
58+
test "#*_before_type_cast with aliased attribute" do
59+
assert_equal @before_type_cast[:salary], @developer.compensation_before_type_cast
60+
end
61+
62+
test "#*_for_database" do
63+
assert_equal @after_type_cast[:salary], @developer.salary_for_database
64+
end
65+
66+
test "#*_for_database with aliased attribute" do
67+
assert_equal @after_type_cast[:salary], @developer.compensation_for_database
68+
end
69+
70+
test "#*_came_from_user?" do
71+
assert_predicate @developer, :salary_came_from_user?
72+
assert_not_predicate Developer.new, :salary_came_from_user?
73+
end
74+
75+
test "#*_came_from_user? with aliased attribute" do
76+
assert_predicate @developer, :compensation_came_from_user?
77+
assert_not_predicate Developer.new, :compensation_came_from_user?
78+
end
79+
end

0 commit comments

Comments
 (0)