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 +4 -4
- data/lib/coaster/serialized_properties.rb +107 -10
- data/lib/coaster/version.rb +1 -1
- data/test/support/models.rb +27 -0
- data/test/support/schema.rb +1 -0
- data/test/test_helper.rb +7 -1
- data/test/test_serialized_property.rb +338 -2
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d69bb86707fa932dcc3782453f4a1eaf5a0f6affb00ae4ad7cb512af8ee5502b
|
|
4
|
+
data.tar.gz: afdd5fee8f322409b3ab0a67c735172d7afadeb243831d5ecc7e759b6381f1f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
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]
|
|
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]
|
|
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
|
|
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
|
-
|
|
318
|
+
hsh.delete(key.to_s)
|
|
224
319
|
else
|
|
225
320
|
val = setter.call(val) if setter
|
|
226
|
-
|
|
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
|
data/lib/coaster/version.rb
CHANGED
data/test/support/models.rb
CHANGED
|
@@ -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
|
data/test/support/schema.rb
CHANGED
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,
|
|
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.
|
|
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-
|
|
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.
|
|
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:
|