Skip to content

Commit 45cf673

Browse files
authored
Merge pull request rails#53359 from kamipo/fix_time_zone_aware_custom_attributes
Fix time zone-aware custom attributes not to hit the circuit breaker for infinite recursion
2 parents baa345e + e875b2d commit 45cf673

File tree

5 files changed

+78
-18
lines changed

5 files changed

+78
-18
lines changed

activemodel/lib/active_model/type/value.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ def force_equality?(_value) # :nodoc:
114114
false
115115
end
116116

117-
def map(value) # :nodoc:
118-
yield value
117+
def map(value, &) # :nodoc:
118+
value
119119
end
120120

121121
def ==(other)

activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def cast(value)
2828
elsif value.respond_to?(:infinite?) && value.infinite?
2929
value
3030
else
31-
map_avoiding_infinite_recursion(super) { |v| cast(v) }
31+
map(super) { |v| cast(v) }
3232
end
3333
end
3434

@@ -45,23 +45,13 @@ def convert_time_to_time_zone(value)
4545
elsif value.respond_to?(:infinite?) && value.infinite?
4646
value
4747
else
48-
map_avoiding_infinite_recursion(value) { |v| convert_time_to_time_zone(v) }
48+
map(value) { |v| convert_time_to_time_zone(v) }
4949
end
5050
end
5151

5252
def set_time_zone_without_conversion(value)
5353
::Time.zone.local_to_utc(value).try(:in_time_zone) if value
5454
end
55-
56-
def map_avoiding_infinite_recursion(value)
57-
map(value) do |v|
58-
if value.equal?(v)
59-
nil
60-
else
61-
yield(v)
62-
end
63-
end
64-
end
6555
end
6656

6757
extend ActiveSupport::Concern

activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def type_cast_for_schema(value)
6565
end
6666

6767
def map(value, &block)
68-
value.map { |v| subtype.map(v, &block) }
68+
value.is_a?(::Array) ? value.map(&block) : subtype.map(value, &block)
6969
end
7070

7171
def changed_in_place?(raw_old_value, new_value)

activerecord/test/cases/attribute_methods_test.rb

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@
1919
class AttributeMethodsTest < ActiveRecord::TestCase
2020
include InTimeZone
2121

22+
class EpochTimestamp < ActiveRecord::Type::DateTime
23+
def deserialize(time_or_int)
24+
Time.at(time_or_int).utc if time_or_int
25+
end
26+
27+
def serialize(time)
28+
time.to_i if time
29+
end
30+
end
31+
32+
ActiveRecord::Type.register(:epoch_timestamp, EpochTimestamp)
33+
2234
fixtures :topics, :developers, :companies, :computers
2335

2436
def setup
@@ -911,9 +923,67 @@ def topic.approved; false; end
911923
end
912924

913925
test "time zone-aware attributes do not recurse infinitely on invalid values" do
926+
model = new_topic_like_ar_class { }
927+
928+
type = model.type_for_attribute(:bonus_time)
929+
assert_kind_of ActiveRecord::Type::Time, type
930+
931+
invalid_time = []
932+
record = model.new(bonus_time: invalid_time)
933+
assert_equal invalid_time, record.bonus_time
934+
935+
invalid_time = Time.current.utc.to_i
936+
record = model.new(bonus_time: invalid_time)
937+
assert_equal invalid_time, record.bonus_time
938+
914939
in_time_zone "Pacific Time (US & Canada)" do
915-
record = @target.new(bonus_time: [])
916-
assert_nil record.bonus_time
940+
model = new_topic_like_ar_class { }
941+
942+
type = model.type_for_attribute(:bonus_time)
943+
assert_kind_of ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter, type
944+
945+
invalid_time = []
946+
record = model.new(bonus_time: invalid_time)
947+
assert_equal invalid_time, record.bonus_time
948+
949+
invalid_time = Time.current.utc.to_i
950+
record = model.new(bonus_time: invalid_time)
951+
assert_equal invalid_time, record.bonus_time
952+
end
953+
end
954+
955+
test "time zone-aware custom attributes" do
956+
timestamp = Time.current.utc.to_i
957+
958+
model = Class.new(ActiveRecord::Base)
959+
model.table_name = "minimalistics"
960+
961+
model.attribute :expires_at, :epoch_timestamp
962+
963+
type = model.type_for_attribute(:expires_at)
964+
assert_kind_of EpochTimestamp, type
965+
966+
record_1 = model.create!(expires_at: timestamp)
967+
assert_equal timestamp, record_1.expires_at.to_i
968+
969+
model.insert!({ expires_at: timestamp })
970+
record_2 = model.last
971+
assert_not_equal record_1, record_2
972+
assert_equal timestamp, record_2.expires_at.to_i
973+
974+
in_time_zone "Pacific Time (US & Canada)" do
975+
model.attribute :expires_at, :epoch_timestamp
976+
977+
type = model.type_for_attribute(:expires_at)
978+
assert_kind_of ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter, type
979+
980+
record_1 = model.create!(expires_at: timestamp)
981+
assert_equal timestamp, record_1.expires_at.to_i
982+
983+
model.insert!({ expires_at: timestamp })
984+
record_2 = model.last
985+
assert_not_equal record_1, record_2
986+
assert_equal timestamp, record_2.expires_at.to_i
917987
end
918988
end
919989

@@ -1433,7 +1503,6 @@ def self.name
14331503
assert_equal "Text", comment.text
14341504
end
14351505

1436-
14371506
test "#alias_attribute with a manually defined method raises an error" do
14381507
class_with_aliased_manually_defined_method = Class.new(ActiveRecord::Base) do
14391508
def self.name

activerecord/test/schema/schema.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,7 @@
809809
end
810810

811811
create_table :minimalistics, force: true do |t|
812+
t.bigint :expires_at
812813
end
813814

814815
create_table :mixed_case_monkeys, force: true, id: false do |t|

0 commit comments

Comments
 (0)