coaster 1.4.37 → 1.4.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bba62b6d537a978d276cce15b24fbadc212264a4f76c2b79957c2db624c82123
4
- data.tar.gz: 11824cc9943176d3cc97cee10976ffe753a532dc72235bd69dacf86af6a9d1fc
3
+ metadata.gz: d69bb86707fa932dcc3782453f4a1eaf5a0f6affb00ae4ad7cb512af8ee5502b
4
+ data.tar.gz: afdd5fee8f322409b3ab0a67c735172d7afadeb243831d5ecc7e759b6381f1f6
5
5
  SHA512:
6
- metadata.gz: c36188aa4545dc5e96d1d30ff2feb8b8324c458ec620dd3de54380eee427d9d2914d07f2b1d03d892790d76f2531f340f237c84765f152a3f95a5a7e5f8503d5
7
- data.tar.gz: 2de119abb062d89d8a5ce7998f19c83c1cae7059f8ae6fc14959e55832662dd8d060fb6591a662cc70ae16729ed26d9b9424a282fa5e583dc59153d1c4320158
6
+ metadata.gz: 57bde1aabfa65503fbdfbc57e9c6c36d673fcd99838dcf914717ce5e09b70be5c4620ee9e3bdc0bb707f02b236cfc0e5cbade535d776bdf3aebab53bddae3ac9
7
+ data.tar.gz: 01da4d3b757a1cba48acdeda80bd3cd6adf0cb0b5f65f5f7592db9fde8b0465680f461467928d69b4870ecc4e780c8174f374598ace29dbe96cd52a441540d16
@@ -17,6 +17,67 @@ module Coaster
17
17
  def sprop_previous_change(key) = sprop_previous_changes[key.to_s]
18
18
  def sprop_previously_changed?(key) = sprop_previous_change(key).present?
19
19
  def sprop_previously_was(key) = (ch = sprop_previous_change(key)).present? ? ch[0] : nil
20
+
21
+ if defined?(ActiveRecord::Base) && base < ActiveRecord::Base
22
+ # Internal method to write serialized property value
23
+ # @param key [String, Symbol] property key
24
+ # @param value [Object] value to write
25
+ # @param apply_setter [Boolean] whether to apply setter proc
26
+ def _write_sprop_value(key, value, apply_setter: true)
27
+ setting = self.class.serialized_property_setting(key)
28
+ return unless setting
29
+ col = setting[:column]
30
+ send("#{col}_will_change!")
31
+ hsh = send(col)
32
+ if value.nil?
33
+ hsh.delete(key.to_s)
34
+ else
35
+ value = setting[:setter].call(value) if apply_setter && setting[:setter]
36
+ hsh[key.to_s] = value
37
+ end
38
+ value
39
+ end
40
+
41
+ def write_attribute(attr_name, value)
42
+ if self.class.serialized_property_setting(attr_name)
43
+ _write_sprop_value(attr_name, value, apply_setter: false)
44
+ else
45
+ super
46
+ end
47
+ end
48
+
49
+ def read_attribute(attr_name)
50
+ if (setting = self.class.serialized_property_setting(attr_name))
51
+ col = setting[:column]
52
+ hsh = super(col) || {}
53
+ val = hsh[attr_name.to_s]
54
+ val = setting[:getter].call(val) if setting[:getter]
55
+ val
56
+ else
57
+ super
58
+ end
59
+ end
60
+
61
+ def [](attr_name)
62
+ if (setting = self.class.serialized_property_setting(attr_name))
63
+ col = setting[:column]
64
+ hsh = super(col) || {}
65
+ val = hsh[attr_name.to_s]
66
+ val = setting[:getter].call(val) if setting[:getter]
67
+ val
68
+ else
69
+ super
70
+ end
71
+ end
72
+
73
+ def []=(attr_name, value)
74
+ if self.class.serialized_property_setting(attr_name)
75
+ _write_sprop_value(attr_name, value, apply_setter: false)
76
+ else
77
+ super
78
+ end
79
+ end
80
+ end
20
81
  end
21
82
  end
22
83
 
@@ -30,14 +91,40 @@ module Coaster
30
91
  end
31
92
  end
32
93
 
94
+ def own_serialized_property_settings
95
+ @own_serialized_property_settings ||= {}
96
+ end
97
+
33
98
  def serialized_property_settings
34
- @serialized_property_settings ||= {}
99
+ @serialized_property_settings ||= if superclass.respond_to?(:serialized_property_settings)
100
+ superclass.serialized_property_settings.dup
101
+ else
102
+ {}
103
+ end
35
104
  end
36
105
 
37
106
  def serialized_property_setting(key)
38
107
  serialized_property_settings[key.to_sym]
39
108
  end
40
109
 
110
+ def set_serialized_property_setting(key, setting)
111
+ own_serialized_property_settings[key.to_sym] = setting
112
+ serialized_property_settings[key.to_sym] = setting
113
+ end
114
+
115
+ def delete_serialized_property_setting(key)
116
+ own_serialized_property_settings.delete(key.to_sym)
117
+ serialized_property_settings.delete(key.to_sym)
118
+ end
119
+
120
+ def rename_serialized_property_setting(from_key, to_key)
121
+ setting = own_serialized_property_settings.delete(from_key.to_sym)
122
+ own_serialized_property_settings[to_key.to_sym] = setting
123
+ serialized_property_settings.delete(from_key.to_sym)
124
+ serialized_property_settings[to_key.to_sym] = setting
125
+ setting
126
+ end
127
+
41
128
  def serialized_column(serialize_column)
42
129
  define_method serialize_column.to_sym do
43
130
  return read_attribute(serialize_column.to_sym) if read_attribute(serialize_column.to_sym)
@@ -53,8 +140,8 @@ module Coaster
53
140
  end
54
141
 
55
142
  def serialized_property(serialize_column, key, type: nil, comment: nil, getter: nil, setter: nil, setter_callback: nil, default: nil, rescuer: nil)
56
- raise DuplicatedProperty, "#{self.name}##{key} duplicated\n#{caller[0..5].join("\n")}" if serialized_property_settings[key.to_sym]
57
- serialized_property_settings[key.to_sym] = {column: serialize_column.to_sym, type: type, comment: comment, getter: getter, setter: setter, setter_callback: setter_callback, default: default, rescuer: rescuer}
143
+ raise DuplicatedProperty, "#{self.name}##{key} duplicated\n#{caller[0..5].join("\n")}" if own_serialized_property_settings[key.to_sym]
144
+ set_serialized_property_setting(key, {column: serialize_column.to_sym, type: type, comment: comment, getter: getter, setter: setter, setter_callback: setter_callback, default: default, rescuer: rescuer})
58
145
  _typed_serialized_property(serialize_column, key, type: type, getter: getter, setter: setter, setter_callback: setter_callback, default: default, rescuer: rescuer)
59
146
  end
60
147
 
@@ -98,7 +185,7 @@ module Coaster
98
185
  end
99
186
  end
100
187
  if type
101
- serialized_property_setting(key.to_sym)[:type] = type
188
+ serialized_property_setting(key)[:type] = type
102
189
  _typed_serialized_property serialize_column, key, type: type, getter: getter, setter: setter, setter_callback: setter_callback, default: default
103
190
  end
104
191
  end
@@ -145,7 +232,7 @@ module Coaster
145
232
  elsif type.respond_to?(:serialized_property_serializer) && (serializer = type.serialized_property_serializer)
146
233
  _define_serialized_property(serialize_column, key, getter: serializer[:getter], setter: serializer[:setter], setter_callback: serializer[:setter_callback], default: default)
147
234
  elsif (type.is_a?(Symbol) && (t = type.to_s.constantize rescue nil)) || (type.is_a?(Class) && type < ActiveRecord::Base && (t = type))
148
- serialized_property_settings["#{key}_id".to_sym] = serialized_property_settings.delete(key.to_sym) # rename key from setting
235
+ rename_serialized_property_setting(key, "#{key}_id")
149
236
  _define_serialized_property serialize_column, "#{key}_id", default: default
150
237
 
151
238
  define_method key.to_sym do
@@ -189,17 +276,22 @@ module Coaster
189
276
  end
190
277
 
191
278
  def _define_serialized_property(serialize_column, key, getter: nil, setter: nil, setter_callback: nil, default: nil)
279
+ is_active_record = defined?(ActiveRecord::Base) && self < ActiveRecord::Base
192
280
  if default
193
281
  if getter
194
282
  define_method key.to_sym do
195
283
  hsh = send(serialize_column.to_sym)
196
- hsh[key.to_s] ||= default.dup
284
+ if hsh[key.to_s].nil?
285
+ hsh[key.to_s] = default.dup
286
+ end
197
287
  getter.call(hsh[key.to_s])
198
288
  end
199
289
  else
200
290
  define_method key.to_sym do
201
291
  hsh = send(serialize_column.to_sym)
202
- hsh[key.to_s] ||= default.dup
292
+ if hsh[key.to_s].nil?
293
+ hsh[key.to_s] = default.dup
294
+ end
203
295
  hsh[key.to_s]
204
296
  end
205
297
  end
@@ -217,20 +309,25 @@ module Coaster
217
309
  end
218
310
  end
219
311
 
220
- if setter
312
+ if is_active_record
221
313
  define_method "#{key}_without_callback=".to_sym do |val|
314
+ col = serialize_column
315
+ send("#{col}_will_change!")
316
+ hsh = send(col)
222
317
  if val.nil?
223
- send(serialize_column.to_sym).delete(key.to_s)
318
+ hsh.delete(key.to_s)
224
319
  else
225
320
  val = setter.call(val) if setter
226
- send(serialize_column.to_sym)[key.to_s] = val
321
+ hsh[key.to_s] = val
227
322
  end
323
+ val
228
324
  end
229
325
  else
230
326
  define_method "#{key}_without_callback=".to_sym do |val|
231
327
  if val.nil?
232
328
  send(serialize_column.to_sym).delete(key.to_s)
233
329
  else
330
+ val = setter.call(val) if setter
234
331
  send(serialize_column.to_sym)[key.to_s] = val
235
332
  end
236
333
  end
@@ -1,3 +1,3 @@
1
1
  module Coaster
2
- VERSION = '1.4.37'
2
+ VERSION = '1.4.38'
3
3
  end
@@ -6,6 +6,7 @@ class User < ActiveRecord::Base
6
6
  extend Coaster::SerializedProperties
7
7
  serialized_column :data
8
8
  serialized_property :data, :appendix, default: {}
9
+ serialized_property :data, :simple
9
10
  serialized_property :data, :father, type: :User
10
11
  serialized_property :data, :mother, type: self
11
12
 
@@ -14,3 +15,29 @@ class User < ActiveRecord::Base
14
15
  appendix['test_key2'] ||= 0
15
16
  end
16
17
  end
18
+
19
+ class Student < User
20
+ serialized_property :data, :grade
21
+ serialized_property :data, :scores, default: {}
22
+
23
+ # Property with getter (transforms on read)
24
+ serialized_property :data, :score_percentage,
25
+ getter: ->(val) { val.nil? ? nil : "#{val}%" }
26
+
27
+ # Property with setter (transforms on write)
28
+ serialized_property :data, :uppercase_name,
29
+ setter: ->(val) { val.to_s.upcase }
30
+
31
+ # Property with both getter and setter
32
+ serialized_property :data, :encrypted_value,
33
+ getter: ->(val) { val.nil? ? nil : Base64.decode64(val) },
34
+ setter: ->(val) { Base64.strict_encode64(val.to_s) }
35
+
36
+ # Property with type: Time (uses ISO8601 string internally)
37
+ serialized_property :data, :enrolled_at, type: Time
38
+
39
+ # Property with type: :unix_epoch (uses integer timestamp internally)
40
+ serialized_property :data, :graduated_at, type: :unix_epoch
41
+
42
+ serialized_property :data, :teacher, type: 'User'
43
+ end
@@ -1,5 +1,6 @@
1
1
  ActiveRecord::Schema.define do
2
2
  create_table :users do |t|
3
+ t.string :type
3
4
  t.string :key
4
5
  t.string :name
5
6
  t.integer :age
data/test/test_helper.rb CHANGED
@@ -10,11 +10,17 @@ require 'bundler/setup'
10
10
  require 'coaster'
11
11
  require 'logger'
12
12
 
13
+ require 'rails'
13
14
  require 'active_record'
15
+
16
+ class TestApp < Rails::Application
17
+ config.eager_load = false
18
+ end
19
+ Rails.application.initialize!
20
+
14
21
  ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ":memory:"
15
22
  load File.expand_path('../support/schema.rb', __FILE__)
16
23
  load File.expand_path('../support/models.rb', __FILE__)
17
- require 'rails'
18
24
 
19
25
  class Raven
20
26
  def self.capture_exception(*args)
@@ -11,7 +11,7 @@ module Coaster
11
11
 
12
12
  def test_serialized
13
13
  user = User.create(name: 'abc')
14
- assert_equal([:appendix, :father_id, :mother_id], User.serialized_property_settings.keys)
14
+ assert_equal([:appendix, :simple, :father_id, :mother_id], User.serialized_property_settings.keys)
15
15
  user.init_appendix
16
16
  assert_equal 0, user.appendix['test_key1']
17
17
  assert_equal 0, user.appendix['test_key2']
@@ -24,7 +24,7 @@ module Coaster
24
24
  assert_equal mother, user.mother
25
25
  assert_equal mother.id, user.mother_id
26
26
  assert_equal({"appendix"=>{"test_key1"=>0, "test_key2"=>0}, "father_id"=>father.id, "mother_id"=>mother.id}, user.data)
27
- assert_equal({"appendix" => [nil, {"test_key1" => 0, "test_key2" => 0}], "father_id" => [nil, 2], "mother_id" => [nil, 3]}, user.sprop_changes)
27
+ assert_equal({"appendix" => [nil, {"test_key1" => 0, "test_key2" => 0}], "father_id" => [nil, father.id], "mother_id" => [nil, mother.id]}, user.sprop_changes)
28
28
  assert_equal(true, user.mother_id_changed?)
29
29
  assert_equal(nil, user.mother_id_was)
30
30
  user.save!
@@ -35,6 +35,342 @@ module Coaster
35
35
  assert_equal({"appendix"=>{"test_key1"=>0, "test_key2"=>0}, "father_id"=>father.id, "mother_id"=>mother.id}, user.data)
36
36
  assert_equal mother, user.mother
37
37
  assert_equal father, user.father
38
+ user.write_attribute(:simple, 100)
39
+ assert_equal(100, user.read_attribute(:simple))
40
+ end
41
+
42
+ def test_active_record_attribute_methods
43
+ user = User.create(name: 'test_ar')
44
+
45
+ # Test write_attribute / read_attribute
46
+ user.write_attribute(:simple, 'test_value')
47
+ assert_equal 'test_value', user.read_attribute(:simple)
48
+ assert_equal 'test_value', user.simple
49
+
50
+ # Test [] / []= accessors
51
+ user[:simple] = 'bracket_value'
52
+ assert_equal 'bracket_value', user[:simple]
53
+ assert_equal 'bracket_value', user.simple
54
+
55
+ # Verify it's stored in the serialized column
56
+ assert_equal 'bracket_value', user.data['simple']
57
+
58
+ # Test that changes are persisted
59
+ user.save!
60
+ user.reload
61
+ assert_equal 'bracket_value', user.simple
62
+ assert_equal 'bracket_value', user.read_attribute(:simple)
63
+ assert_equal 'bracket_value', user[:simple]
64
+ end
65
+
66
+ def test_dirty_tracking_with_will_change
67
+ user = User.create(name: 'test_dirty')
68
+
69
+ # Initially no changes
70
+ assert_equal false, user.simple_changed?
71
+
72
+ # After setting value, should be marked as changed
73
+ user.simple = 'new_value'
74
+ assert_equal true, user.simple_changed?
75
+ assert_equal [nil, 'new_value'], user.simple_change
76
+
77
+ # Save and verify previous change tracking
78
+ user.save!
79
+ assert_equal false, user.simple_changed?
80
+ assert_equal true, user.simple_previously_changed?
81
+ assert_equal [nil, 'new_value'], user.simple_previous_change
82
+
83
+ # Update again
84
+ user.simple = 'updated_value'
85
+ assert_equal true, user.simple_changed?
86
+ assert_equal 'new_value', user.simple_was
87
+ user.save!
88
+ assert_equal 'new_value', user.simple_previously_was
89
+ end
90
+
91
+ def test_default_value_persistence
92
+ user = User.create(name: 'test_default')
93
+
94
+ # appendix has default: {}
95
+ # Modifying the returned hash should persist to data
96
+ user.appendix['key1'] = 'value1'
97
+ assert_equal 'value1', user.appendix['key1']
98
+ assert_equal({'key1' => 'value1'}, user.data['appendix'])
99
+
100
+ # Verify persistence after save
101
+ user.save!
102
+ user.reload
103
+ assert_equal 'value1', user.appendix['key1']
104
+ end
105
+
106
+ def test_inherited_model_serialized_properties
107
+ # Student inherits from User and has its own serialized properties
108
+ student = Student.create(name: 'test_student')
109
+
110
+ # Should have access to parent's serialized properties
111
+ student.simple = 'inherited_value'
112
+ assert_equal 'inherited_value', student.simple
113
+
114
+ # Should have access to parent's default property
115
+ student.appendix['parent_key'] = 'parent_value'
116
+ assert_equal 'parent_value', student.appendix['parent_key']
117
+
118
+ # Should have access to own serialized properties
119
+ student.grade = 'A'
120
+ assert_equal 'A', student.grade
121
+
122
+ # Should have access to own default property
123
+ student.scores['math'] = 95
124
+ student.scores['english'] = 88
125
+ assert_equal 95, student.scores['math']
126
+ assert_equal 88, student.scores['english']
127
+
128
+ # Verify data structure
129
+ assert_equal 'inherited_value', student.data['simple']
130
+ assert_equal 'A', student.data['grade']
131
+ assert_equal({'parent_key' => 'parent_value'}, student.data['appendix'])
132
+ assert_equal({'math' => 95, 'english' => 88}, student.data['scores'])
133
+
134
+ # Verify persistence
135
+ student.save!
136
+ student.reload
137
+ assert_equal 'inherited_value', student.simple
138
+ assert_equal 'A', student.grade
139
+ assert_equal 'parent_value', student.appendix['parent_key']
140
+ assert_equal 95, student.scores['math']
141
+ end
142
+
143
+ def test_inherited_model_attribute_methods
144
+ student = Student.create(name: 'test_student_ar')
145
+
146
+ # Test write_attribute / read_attribute for inherited property
147
+ student.write_attribute(:simple, 'write_test')
148
+ assert_equal 'write_test', student.read_attribute(:simple)
149
+
150
+ # Test write_attribute / read_attribute for own property
151
+ student.write_attribute(:grade, 'B+')
152
+ assert_equal 'B+', student.read_attribute(:grade)
153
+
154
+ # Test [] / []= for inherited property
155
+ student[:simple] = 'bracket_inherited'
156
+ assert_equal 'bracket_inherited', student[:simple]
157
+
158
+ # Test [] / []= for own property
159
+ student[:grade] = 'A-'
160
+ assert_equal 'A-', student[:grade]
161
+ end
162
+
163
+ def test_inherited_model_dirty_tracking
164
+ student = Student.create(name: 'test_student_dirty')
165
+
166
+ # Test dirty tracking for inherited property
167
+ student.simple = 'new_simple'
168
+ assert_equal true, student.simple_changed?
169
+
170
+ # Test dirty tracking for own property
171
+ student.grade = 'A'
172
+ assert_equal true, student.grade_changed?
173
+
174
+ student.save!
175
+ assert_equal false, student.simple_changed?
176
+ assert_equal false, student.grade_changed?
177
+ assert_equal true, student.simple_previously_changed?
178
+ assert_equal true, student.grade_previously_changed?
179
+ end
180
+
181
+ def test_inherited_model_settings
182
+ # Student should have both parent's and own property settings via serialized_property_setting
183
+ assert Student.serialized_property_setting(:appendix), 'should access parent appendix'
184
+ assert Student.serialized_property_setting(:simple), 'should access parent simple'
185
+ assert Student.serialized_property_setting(:grade), 'should access own grade'
186
+ assert Student.serialized_property_setting(:scores), 'should access own scores'
187
+
188
+ # serialized_property_settings should include both parent and own settings
189
+ all_settings = Student.serialized_property_settings
190
+ assert all_settings[:appendix], 'should include parent appendix'
191
+ assert all_settings[:simple], 'should include parent simple'
192
+ assert all_settings[:grade], 'should include own grade'
193
+ assert all_settings[:scores], 'should include own scores'
194
+
195
+ # own_serialized_property_settings should only include own settings
196
+ own_settings = Student.own_serialized_property_settings
197
+ assert_nil own_settings[:appendix], 'should not include parent appendix'
198
+ assert_nil own_settings[:simple], 'should not include parent simple'
199
+ assert own_settings[:grade], 'should include own grade'
200
+ assert own_settings[:scores], 'should include own scores'
201
+ end
202
+
203
+ def test_getter_transforms_on_read
204
+ student = Student.create(name: 'test_getter')
205
+
206
+ # Set raw value via write_attribute (bypasses setter)
207
+ student.write_attribute(:score_percentage, 85)
208
+ assert_equal '85%', student.score_percentage
209
+
210
+ # Verify raw value in data
211
+ assert_equal 85, student.data['score_percentage']
212
+
213
+ # Persistence
214
+ student.save!
215
+ student.reload
216
+ assert_equal '85%', student.score_percentage
217
+ end
218
+
219
+ def test_setter_transforms_on_write
220
+ student = Student.create(name: 'test_setter')
221
+
222
+ # Set via setter - should transform to uppercase
223
+ student.uppercase_name = 'john doe'
224
+ assert_equal 'JOHN DOE', student.uppercase_name
225
+ assert_equal 'JOHN DOE', student.data['uppercase_name']
226
+
227
+ # Persistence
228
+ student.save!
229
+ student.reload
230
+ assert_equal 'JOHN DOE', student.uppercase_name
231
+ end
232
+
233
+ def test_write_attribute_bypasses_setter
234
+ student = Student.create(name: 'test_write_attr')
235
+
236
+ # write_attribute should NOT apply setter transformation
237
+ student.write_attribute(:uppercase_name, 'lowercase')
238
+ assert_equal 'lowercase', student.uppercase_name
239
+ assert_equal 'lowercase', student.data['uppercase_name']
240
+
241
+ # But setter should apply transformation
242
+ student.uppercase_name = 'another value'
243
+ assert_equal 'ANOTHER VALUE', student.uppercase_name
244
+ end
245
+
246
+ def test_getter_and_setter_together
247
+ student = Student.create(name: 'test_both')
248
+
249
+ # Set value - setter encodes to Base64
250
+ student.encrypted_value = 'secret data'
251
+
252
+ # Raw value should be Base64 encoded
253
+ raw_value = student.data['encrypted_value']
254
+ assert_equal 'c2VjcmV0IGRhdGE=', raw_value
255
+
256
+ # Getter should decode
257
+ assert_equal 'secret data', student.encrypted_value
258
+
259
+ # Persistence
260
+ student.save!
261
+ student.reload
262
+ assert_equal 'secret data', student.encrypted_value
263
+ assert_equal 'c2VjcmV0IGRhdGE=', student.data['encrypted_value']
264
+ end
265
+
266
+ def test_time_type_property
267
+ Time.zone = 'UTC'
268
+ student = Student.create(name: 'test_time')
269
+ time = Time.zone.parse('2024-06-15 10:30:00 UTC')
270
+
271
+ # Use the _without_callback= method which applies setter via _define_serialized_property
272
+ student.enrolled_at_without_callback = time
273
+
274
+ # Raw value should be ISO8601 string (setter transforms Time to string)
275
+ raw_value = student.data['enrolled_at']
276
+ assert raw_value.is_a?(String), "Expected String but got #{raw_value.class}"
277
+ assert raw_value.include?('2024-06-15')
278
+
279
+ # getter transforms string back to Time
280
+ assert_equal time.to_i, student.enrolled_at.to_i
281
+
282
+ # Persistence
283
+ student.save!
284
+ student.reload
285
+ assert_equal time.to_i, student.enrolled_at.to_i
286
+ end
287
+
288
+ def test_unix_epoch_type_property
289
+ Time.zone = 'UTC'
290
+ student = Student.create(name: 'test_unix')
291
+ time = Time.zone.parse('2024-06-15 10:30:00 UTC')
292
+
293
+ # Use the _without_callback= method which applies setter
294
+ student.graduated_at = time
295
+
296
+ # Raw value should be integer timestamp (setter transforms Time to integer)
297
+ raw_value = student.data['graduated_at']
298
+ assert raw_value.is_a?(Integer), "Expected Integer but got #{raw_value.class}"
299
+ assert_equal time.to_i, raw_value
300
+
301
+ # getter transforms integer back to Time
302
+ assert_equal time.to_i, student.graduated_at.to_i
303
+
304
+ # Persistence
305
+ student.save!
306
+ student.reload
307
+ assert_equal time.to_i, student.graduated_at.to_i
308
+ end
309
+
310
+ def test_time_type_with_direct_data_assignment
311
+ Time.zone = 'UTC'
312
+ student = Student.create(name: 'test_time_direct')
313
+
314
+ student.enrolled_at = Time.parse('2024-01-01T00:00:00.000000Z')
315
+ assert '2024-01-01T00:00:00.000000Z', student.data['enrolled_at']
316
+ student.graduated_at = Time.parse('2028-01-01T00:00:00.000000Z')
317
+ assert Time.parse('2028-01-01T00:00:00.000000Z').to_i, student.data['graduated_at']
318
+
319
+ # getter transforms string to Time
320
+ assert student.enrolled_at.is_a?(ActiveSupport::TimeWithZone)
321
+ assert_equal 2024, student.enrolled_at.year
322
+
323
+ teacher = User.create(name: 'Dr john')
324
+ student.teacher = teacher
325
+ student.save
326
+ student = User.find(student.id)
327
+ assert teacher.id, student.data['teacher_id']
328
+ assert User, student.teacher.class
329
+ assert teacher.id, student.teacher.id
330
+ end
331
+
332
+ def test_read_attribute_applies_getter
333
+ student = Student.create(name: 'test_read_getter')
334
+
335
+ # Set raw value directly in data
336
+ student.data['score_percentage'] = 85
337
+
338
+ # read_attribute should apply getter (adds %)
339
+ assert_equal '85%', student.read_attribute(:score_percentage)
340
+
341
+ # Same with [] accessor
342
+ assert_equal '85%', student[:score_percentage]
343
+ end
344
+
345
+ def test_read_attribute_with_time_type
346
+ Time.zone = 'UTC'
347
+ student = Student.create(name: 'test_read_time')
348
+
349
+ # Set ISO8601 string directly
350
+ student.data['enrolled_at'] = '2024-06-15T10:30:00.000000Z'
351
+
352
+ # Note: type: Time creates getter in _define_serialized_property,
353
+ # but it's not stored in settings, so read_attribute returns raw value
354
+ # The property accessor (enrolled_at) applies the getter
355
+ result = student.read_attribute(:enrolled_at)
356
+ assert result.is_a?(String), "read_attribute returns raw value for type: Time"
357
+
358
+ # Property accessor applies getter
359
+ result2 = student.enrolled_at
360
+ assert result2.is_a?(ActiveSupport::TimeWithZone)
361
+ assert_equal 2024, result2.year
362
+ end
363
+
364
+ def test_nil_value_with_getter_setter
365
+ student = Student.create(name: 'test_nil')
366
+
367
+ # Set then clear
368
+ student.encrypted_value = 'some value'
369
+ assert_equal 'some value', student.encrypted_value
370
+
371
+ student.encrypted_value = nil
372
+ assert_nil student.encrypted_value
373
+ assert_nil student.data['encrypted_value']
38
374
  end
39
375
  end
40
376
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coaster
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.37
4
+ version: 1.4.38
5
5
  platform: ruby
6
6
  authors:
7
7
  - buzz jung
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-11-20 00:00:00.000000000 Z
10
+ date: 2025-12-10 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: oj
@@ -257,7 +257,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
257
257
  - !ruby/object:Gem::Version
258
258
  version: '0'
259
259
  requirements: []
260
- rubygems_version: 3.6.9
260
+ rubygems_version: 3.6.7
261
261
  specification_version: 4
262
262
  summary: A little convenient feature for standard library
263
263
  test_files: