iknow_view_models 2.8.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +115 -0
  3. data/.gitignore +36 -0
  4. data/.travis.yml +31 -0
  5. data/Appraisals +9 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +19 -0
  9. data/Rakefile +21 -0
  10. data/appveyor.yml +22 -0
  11. data/gemfiles/rails_5_2.gemfile +15 -0
  12. data/gemfiles/rails_6_0_beta.gemfile +15 -0
  13. data/iknow_view_models.gemspec +49 -0
  14. data/lib/iknow_view_models.rb +12 -0
  15. data/lib/iknow_view_models/railtie.rb +8 -0
  16. data/lib/iknow_view_models/version.rb +5 -0
  17. data/lib/view_model.rb +333 -0
  18. data/lib/view_model/access_control.rb +154 -0
  19. data/lib/view_model/access_control/composed.rb +216 -0
  20. data/lib/view_model/access_control/open.rb +13 -0
  21. data/lib/view_model/access_control/read_only.rb +13 -0
  22. data/lib/view_model/access_control/tree.rb +264 -0
  23. data/lib/view_model/access_control_error.rb +10 -0
  24. data/lib/view_model/active_record.rb +383 -0
  25. data/lib/view_model/active_record/association_data.rb +178 -0
  26. data/lib/view_model/active_record/association_manipulation.rb +389 -0
  27. data/lib/view_model/active_record/cache.rb +265 -0
  28. data/lib/view_model/active_record/cache/cacheable_view.rb +51 -0
  29. data/lib/view_model/active_record/cloner.rb +113 -0
  30. data/lib/view_model/active_record/collection_nested_controller.rb +100 -0
  31. data/lib/view_model/active_record/controller.rb +77 -0
  32. data/lib/view_model/active_record/controller_base.rb +185 -0
  33. data/lib/view_model/active_record/nested_controller_base.rb +93 -0
  34. data/lib/view_model/active_record/singular_nested_controller.rb +34 -0
  35. data/lib/view_model/active_record/update_context.rb +252 -0
  36. data/lib/view_model/active_record/update_data.rb +749 -0
  37. data/lib/view_model/active_record/update_operation.rb +810 -0
  38. data/lib/view_model/active_record/visitor.rb +77 -0
  39. data/lib/view_model/after_transaction_runner.rb +29 -0
  40. data/lib/view_model/callbacks.rb +219 -0
  41. data/lib/view_model/changes.rb +62 -0
  42. data/lib/view_model/config.rb +29 -0
  43. data/lib/view_model/controller.rb +142 -0
  44. data/lib/view_model/deserialization_error.rb +437 -0
  45. data/lib/view_model/deserialize_context.rb +16 -0
  46. data/lib/view_model/error.rb +191 -0
  47. data/lib/view_model/error_view.rb +35 -0
  48. data/lib/view_model/record.rb +367 -0
  49. data/lib/view_model/record/attribute_data.rb +48 -0
  50. data/lib/view_model/reference.rb +31 -0
  51. data/lib/view_model/references.rb +48 -0
  52. data/lib/view_model/registry.rb +73 -0
  53. data/lib/view_model/schemas.rb +45 -0
  54. data/lib/view_model/serialization_error.rb +10 -0
  55. data/lib/view_model/serialize_context.rb +118 -0
  56. data/lib/view_model/test_helpers.rb +103 -0
  57. data/lib/view_model/test_helpers/arvm_builder.rb +111 -0
  58. data/lib/view_model/traversal_context.rb +126 -0
  59. data/lib/view_model/utils.rb +24 -0
  60. data/lib/view_model/utils/collections.rb +49 -0
  61. data/test/helpers/arvm_test_models.rb +59 -0
  62. data/test/helpers/arvm_test_utilities.rb +187 -0
  63. data/test/helpers/callback_tracer.rb +27 -0
  64. data/test/helpers/controller_test_helpers.rb +270 -0
  65. data/test/helpers/match_enumerator.rb +58 -0
  66. data/test/helpers/query_logging.rb +71 -0
  67. data/test/helpers/test_access_control.rb +56 -0
  68. data/test/helpers/viewmodel_spec_helpers.rb +326 -0
  69. data/test/unit/view_model/access_control_test.rb +769 -0
  70. data/test/unit/view_model/active_record/alias_test.rb +35 -0
  71. data/test/unit/view_model/active_record/belongs_to_test.rb +376 -0
  72. data/test/unit/view_model/active_record/cache_test.rb +351 -0
  73. data/test/unit/view_model/active_record/cloner_test.rb +313 -0
  74. data/test/unit/view_model/active_record/controller_test.rb +561 -0
  75. data/test/unit/view_model/active_record/counter_test.rb +80 -0
  76. data/test/unit/view_model/active_record/customization_test.rb +388 -0
  77. data/test/unit/view_model/active_record/has_many_test.rb +957 -0
  78. data/test/unit/view_model/active_record/has_many_through_poly_test.rb +269 -0
  79. data/test/unit/view_model/active_record/has_many_through_test.rb +736 -0
  80. data/test/unit/view_model/active_record/has_one_test.rb +334 -0
  81. data/test/unit/view_model/active_record/namespacing_test.rb +75 -0
  82. data/test/unit/view_model/active_record/optional_attribute_view_test.rb +58 -0
  83. data/test/unit/view_model/active_record/poly_test.rb +320 -0
  84. data/test/unit/view_model/active_record/shared_test.rb +285 -0
  85. data/test/unit/view_model/active_record/version_test.rb +121 -0
  86. data/test/unit/view_model/active_record_test.rb +542 -0
  87. data/test/unit/view_model/callbacks_test.rb +582 -0
  88. data/test/unit/view_model/deserialization_error/unique_violation_test.rb +73 -0
  89. data/test/unit/view_model/record_test.rb +524 -0
  90. data/test/unit/view_model/traversal_context_test.rb +371 -0
  91. data/test/unit/view_model_test.rb +62 -0
  92. metadata +490 -0
@@ -0,0 +1,35 @@
1
+ require_relative "../../../helpers/arvm_test_utilities.rb"
2
+ require_relative "../../../helpers/arvm_test_models.rb"
3
+ require_relative "../../../helpers/viewmodel_spec_helpers.rb"
4
+
5
+ require "minitest/autorun"
6
+
7
+ require "view_model/active_record"
8
+
9
+ class ViewModel::ActiveRecord::Alias < ActiveSupport::TestCase
10
+ include ARVMTestUtilities
11
+ extend Minitest::Spec::DSL
12
+
13
+ include ViewModelSpecHelpers::ParentAndBelongsToChild
14
+
15
+ def child_attributes
16
+ super.merge(
17
+ viewmodel: ->(v) do
18
+ add_view_alias "ChildA"
19
+ add_view_alias "ChildB"
20
+ end
21
+ )
22
+ end
23
+
24
+ it "permits association types to be aliased" do
25
+ %w(Child ChildA ChildB).each do |view_alias|
26
+ view = {
27
+ "_type" => viewmodel_class.view_name,
28
+ "child" => { "_type" => view_alias },
29
+ }
30
+
31
+ parent = viewmodel_class.deserialize_from_view(view).model
32
+ assert_instance_of(child_model_class, parent.child)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,376 @@
1
+ require_relative "../../../helpers/arvm_test_utilities.rb"
2
+ require_relative "../../../helpers/arvm_test_models.rb"
3
+
4
+ require "minitest/autorun"
5
+
6
+ require "view_model/active_record"
7
+
8
+ class ViewModel::ActiveRecord::BelongsToTest < ActiveSupport::TestCase
9
+ include ARVMTestUtilities
10
+
11
+ module WithLabel
12
+ def before_all
13
+ super
14
+
15
+ build_viewmodel(:Label) do
16
+ define_schema do |t|
17
+ t.string :text
18
+ end
19
+
20
+ define_model do
21
+ has_one :parent, inverse_of: :label
22
+ end
23
+
24
+ define_viewmodel do
25
+ attributes :text
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ module WithParent
32
+ def before_all
33
+ super
34
+
35
+ build_viewmodel(:Parent) do
36
+ define_schema do |t|
37
+ t.string :name
38
+ t.references :label, foreign_key: true
39
+ end
40
+
41
+ define_model do
42
+ belongs_to :label, inverse_of: :parent, dependent: :destroy
43
+ end
44
+
45
+ define_viewmodel do
46
+ attributes :name
47
+ associations :label
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ module WithOwner
54
+ def before_all
55
+ super
56
+
57
+ build_viewmodel(:Owner) do
58
+ define_schema do |t|
59
+ t.integer :deleted_id
60
+ t.integer :ignored_id
61
+ end
62
+
63
+ define_model do
64
+ belongs_to :deleted, class_name: Label.name, dependent: :delete
65
+ belongs_to :ignored, class_name: Label.name
66
+ end
67
+
68
+ define_viewmodel do
69
+ associations :deleted, :ignored
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ include WithLabel
76
+ include WithParent
77
+
78
+ def setup
79
+ super
80
+
81
+ # TODO make a `has_list?` that allows a parent to set all children as an array
82
+ @parent1 = Parent.new(name: "p1",
83
+ label: Label.new(text: "p1l"))
84
+ @parent1.save!
85
+
86
+ @parent2 = Parent.new(name: "p2",
87
+ label: Label.new(text: "p2l"))
88
+
89
+ @parent2.save!
90
+
91
+ enable_logging!
92
+ end
93
+
94
+ def test_serialize_view
95
+ view, _refs = serialize_with_references(ParentView.new(@parent1))
96
+
97
+ assert_equal({ "_type" => "Parent",
98
+ "_version" => 1,
99
+ "id" => @parent1.id,
100
+ "name" => @parent1.name,
101
+ "label" => { "_type" => "Label",
102
+ "_version" => 1,
103
+ "id" => @parent1.label.id,
104
+ "text" => @parent1.label.text },
105
+ },
106
+ view)
107
+ end
108
+
109
+ def test_loading_batching
110
+ log_queries do
111
+ serialize(ParentView.load)
112
+ end
113
+
114
+ assert_equal(['Parent Load', 'Label Load'],
115
+ logged_load_queries)
116
+ end
117
+
118
+ def test_create_from_view
119
+ view = {
120
+ "_type" => "Parent",
121
+ "name" => "p",
122
+ "label" => { "_type" => "Label", "text" => "l" },
123
+ }
124
+
125
+ pv = ParentView.deserialize_from_view(view)
126
+ p = pv.model
127
+
128
+ assert(!p.changed?)
129
+ assert(!p.new_record?)
130
+
131
+ assert_equal("p", p.name)
132
+
133
+ assert(p.label.present?)
134
+ assert_equal("l", p.label.text)
135
+ end
136
+
137
+ def test_create_belongs_to_nil
138
+ view = { '_type' => 'Parent', 'name' => 'p', 'label' => nil }
139
+ pv = ParentView.deserialize_from_view(view)
140
+ assert_nil(pv.model.label)
141
+ end
142
+
143
+ def test_create_invalid_child_type
144
+ view = { '_type' => 'Parent', 'name' => 'p', 'label' => { '_type' => 'Parent', 'name' => 'q' } }
145
+ assert_raises(ViewModel::DeserializationError::InvalidAssociationType) do
146
+ ParentView.deserialize_from_view(view)
147
+ end
148
+ end
149
+
150
+ def test_belongs_to_create
151
+ @parent1.update(label: nil)
152
+
153
+ alter_by_view!(ParentView, @parent1) do |view, refs|
154
+ view['label'] = { '_type' => 'Label', 'text' => 'cheese' }
155
+ end
156
+
157
+ assert_equal('cheese', @parent1.label.text)
158
+ end
159
+
160
+ def test_belongs_to_replace
161
+ old_label = @parent1.label
162
+
163
+ alter_by_view!(ParentView, @parent1) do |view, refs|
164
+ view['label'] = { '_type' => 'Label', 'text' => 'cheese' }
165
+ end
166
+
167
+ assert_equal('cheese', @parent1.label.text)
168
+ assert(Label.where(id: old_label).blank?)
169
+ end
170
+
171
+ def test_belongs_to_move_and_replace
172
+ old_p1_label = @parent1.label
173
+ old_p2_label = @parent2.label
174
+
175
+ set_by_view!(ParentView, [@parent1, @parent2]) do |(p1, p2), refs|
176
+ p1['label'] = nil
177
+ p2['label'] = update_hash_for(LabelView, old_p1_label)
178
+ end
179
+
180
+ assert(@parent1.label.blank?, 'l1 label reference removed')
181
+ assert_equal(old_p1_label, @parent2.label, 'p2 has label from p1')
182
+ assert(Label.where(id: old_p2_label).blank?, 'p2 old label deleted')
183
+ end
184
+
185
+ def test_belongs_to_swap
186
+ old_p1_label = @parent1.label
187
+ old_p2_label = @parent2.label
188
+
189
+ alter_by_view!(ParentView, [@parent1, @parent2]) do |(p1, p2), refs|
190
+ p1['label'] = update_hash_for(LabelView, old_p2_label)
191
+ p2['label'] = update_hash_for(LabelView, old_p1_label)
192
+ end
193
+
194
+ assert_equal(old_p2_label, @parent1.label, 'p1 has label from p2')
195
+ assert_equal(old_p1_label, @parent2.label, 'p2 has label from p1')
196
+ end
197
+
198
+ def test_moved_child_is_not_delete_checked
199
+ # move from p1 to p3
200
+ d_context = ParentView.new_deserialize_context
201
+
202
+ target_label = Label.create
203
+ from_parent = Parent.create(name: 'from', label: target_label)
204
+ to_parent = Parent.create(name: 'p3')
205
+
206
+ alter_by_view!(
207
+ ParentView, [from_parent, to_parent],
208
+ deserialize_context: d_context
209
+ ) do |(from, to), refs|
210
+ from['label'] = nil
211
+ to['label'] = update_hash_for(LabelView, target_label)
212
+ end
213
+
214
+ assert_equal(target_label, to_parent.label, 'target label moved')
215
+ assert_equal([ViewModel::Reference.new(ParentView, from_parent.id),
216
+ ViewModel::Reference.new(ParentView, to_parent.id)],
217
+ d_context.valid_edit_refs,
218
+ "only parents are checked for change; child was not")
219
+ end
220
+
221
+ def test_implicit_release_invalid_belongs_to
222
+ taken_label_ref = update_hash_for(LabelView, @parent1.label)
223
+ assert_raises(ViewModel::DeserializationError::ParentNotFound) do
224
+ ParentView.deserialize_from_view(
225
+ [{ '_type' => 'Parent',
226
+ 'name' => 'newp',
227
+ 'label' => taken_label_ref }])
228
+ end
229
+ end
230
+
231
+ class GCTests < ActiveSupport::TestCase
232
+ include ARVMTestUtilities
233
+ include WithLabel
234
+ include WithOwner
235
+ include WithParent
236
+
237
+ # test belongs_to garbage collection - dependent: delete_all
238
+ def test_gc_dependent_delete_all
239
+ owner = Owner.create(deleted: Label.new(text: 'one'))
240
+ old_label = owner.deleted
241
+
242
+ alter_by_view!(OwnerView, owner) do |ov, refs|
243
+ ov['deleted'] = { '_type' => 'Label', 'text' => 'two' }
244
+ end
245
+
246
+ assert_equal('two', owner.deleted.text)
247
+ refute_equal(old_label, owner.deleted)
248
+ assert(Label.where(id: old_label.id).blank?)
249
+ end
250
+
251
+ def test_no_gc_dependent_ignore
252
+ owner = Owner.create(ignored: Label.new(text: "one"))
253
+ old_label = owner.ignored
254
+
255
+ alter_by_view!(OwnerView, owner) do |ov, refs|
256
+ ov['ignored'] = { '_type' => 'Label', 'text' => 'two' }
257
+ end
258
+ assert_equal('two', owner.ignored.text)
259
+ refute_equal(old_label, owner.ignored)
260
+ assert_equal(1, Label.where(id: old_label.id).count)
261
+ end
262
+ end
263
+
264
+ class RenamedTest < ActiveSupport::TestCase
265
+ include ARVMTestUtilities
266
+ include WithLabel
267
+
268
+ def before_all
269
+ super
270
+
271
+ build_viewmodel(:Parent) do
272
+ define_schema do |t|
273
+ t.string :name
274
+ t.references :label, foreign_key: true
275
+ end
276
+
277
+ define_model do
278
+ belongs_to :label, inverse_of: :parent, dependent: :destroy
279
+ end
280
+
281
+ define_viewmodel do
282
+ attributes :name
283
+ association :label, as: :something_else
284
+ end
285
+ end
286
+ end
287
+
288
+ def setup
289
+ super
290
+
291
+ @parent = Parent.create(name: 'p1', label: Label.new(text: 'l1'))
292
+
293
+ enable_logging!
294
+ end
295
+
296
+ def test_dependencies
297
+ root_updates, _ref_updates = ViewModel::ActiveRecord::UpdateData.parse_hashes([{ '_type' => 'Parent', 'something_else' => nil }])
298
+ assert_equal(DeepPreloader::Spec.new('label' => DeepPreloader::Spec.new), root_updates.first.preload_dependencies)
299
+ assert_equal({ 'something_else' => {} }, root_updates.first.updated_associations)
300
+ end
301
+
302
+ def test_renamed_roundtrip
303
+ alter_by_view!(ParentView, @parent) do |view, refs|
304
+ assert_equal({ 'id' => @parent.label.id,
305
+ '_type' => 'Label',
306
+ '_version' => 1,
307
+ 'text' => 'l1' },
308
+ view['something_else'])
309
+ view['something_else']['text'] = 'new l1 text'
310
+ end
311
+ assert_equal('new l1 text', @parent.label.text)
312
+ end
313
+ end
314
+
315
+ class FreedChildrenTest < ActiveSupport::TestCase
316
+ include ARVMTestUtilities
317
+
318
+ def before_all
319
+ build_viewmodel(:Aye) do
320
+ define_schema do |t|
321
+ t.references :bee
322
+ end
323
+ define_model do
324
+ belongs_to :bee, inverse_of: :aye, dependent: :destroy
325
+ end
326
+ define_viewmodel do
327
+ association :bee
328
+ end
329
+ end
330
+
331
+ build_viewmodel(:Bee) do
332
+ define_schema do |t|
333
+ t.references :cee
334
+ end
335
+ define_model do
336
+ has_one :aye, inverse_of: :bee
337
+ belongs_to :cee, inverse_of: :bee, dependent: :destroy
338
+ end
339
+ define_viewmodel do
340
+ association :cee
341
+ end
342
+ end
343
+
344
+ build_viewmodel(:Cee) do
345
+ define_schema do |t|
346
+ end
347
+ define_model do
348
+ has_one :bee, inverse_of: :cee
349
+ end
350
+ define_viewmodel do
351
+ end
352
+ end
353
+ end
354
+
355
+
356
+ # Do we support replacing a node in the tree and reparenting its children
357
+ # back to it? In theory we want to, but currently we don't: the child node
358
+ # is unresolvable.
359
+
360
+ # To support it we could maintain a list of child elements that will be
361
+ # implicitly freed by each freelist entry. Then worklist entries could
362
+ # resolve themselves from these children, and nil out the association target
363
+ # in the freelist to prevent them from being deleted when the freelist is
364
+ # cleaned. If the freelist entry is subsequently reclaimed, double update
365
+ # protection should prevent the child from being reused, but that will need
366
+ # testing.
367
+ def test_move
368
+ model = Aye.create(bee: Bee.new(cee: Cee.new))
369
+ assert_raises(ViewModel::DeserializationError::ParentNotFound) do
370
+ alter_by_view!(AyeView, model) do |view, refs|
371
+ view['bee'].delete("id")
372
+ end
373
+ end
374
+ end
375
+ end
376
+ end
@@ -0,0 +1,351 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "minitest/unit"
5
+ require "minitest/hooks"
6
+
7
+ require_relative "../../../helpers/arvm_test_models.rb"
8
+ require_relative "../../../helpers/arvm_test_utilities.rb"
9
+ require_relative "../../../helpers/viewmodel_spec_helpers.rb"
10
+
11
+ require "view_model"
12
+ require "view_model/active_record"
13
+
14
+ # IknowCache uses Rails.cache: create a dummy cache.
15
+
16
+ DUMMY_RAILS_CACHE = ActiveSupport::Cache::MemoryStore.new
17
+
18
+ module Rails
19
+ def self.cache
20
+ DUMMY_RAILS_CACHE
21
+ end
22
+ end
23
+
24
+ class ViewModel::ActiveRecord
25
+ class CacheTest < ActiveSupport::TestCase
26
+ using ViewModel::Utils::Collections
27
+ extend Minitest::Spec::DSL
28
+ include ARVMTestUtilities
29
+
30
+ # Defines a cacheable parent Model with a owned Child and a cachable shared Shared.
31
+ module CacheableParentAndChildren
32
+ extend ActiveSupport::Concern
33
+ include ViewModelSpecHelpers::ParentAndBelongsToChild
34
+
35
+ def model_attributes
36
+ super.merge(
37
+ schema: ->(t) { t.references :shared, foreign_key: true },
38
+ model: ->(_) { belongs_to :shared, inverse_of: :models },
39
+ viewmodel: ->(_) {
40
+ association :shared, shared: true, optional: false
41
+ cacheable!
42
+ }
43
+ )
44
+ end
45
+
46
+ def shared_cache_group
47
+ @shared_cache_group ||= IknowCache.register_group(:shared, :id)
48
+ end
49
+
50
+ def shared_viewmodel_class
51
+ shared_cache_group = self.shared_cache_group
52
+ @shared_viewmodel_class ||= define_viewmodel_class(:Shared, namespace: namespace) do
53
+ define_schema do |t|
54
+ t.string :name
55
+ end
56
+
57
+ define_model do
58
+ has_many :models
59
+ end
60
+
61
+ define_viewmodel do
62
+ attributes :name
63
+ cacheable!(cache_group: shared_cache_group)
64
+ end
65
+ end
66
+ end
67
+
68
+ def shared_model_class
69
+ shared_viewmodel_class.model_class
70
+ end
71
+
72
+ # parent depends on children, ensure it's touched first
73
+ def viewmodel_class
74
+ shared_viewmodel_class
75
+ super
76
+ end
77
+
78
+ included do
79
+ let(:shared) { shared_model_class.create!(name: "shared1") }
80
+ let(:root) { model_class.create!(name: "root1", child: Child.new(name: "owned1"), shared: shared) }
81
+ let(:root_view) { viewmodel_class.new(root) }
82
+ end
83
+ end
84
+
85
+ before(:each) do
86
+ DUMMY_RAILS_CACHE.clear
87
+ end
88
+
89
+ # Extract the iKnowCaches to verify their contents
90
+ def read_cache(viewmodel_class, id)
91
+ vm_cache = viewmodel_class.viewmodel_cache
92
+ vm_cache.send(:cache).read(vm_cache.key_for(id))
93
+ end
94
+
95
+ def serialize_from_database
96
+ view = viewmodel_class.new(model_class.find(root.id))
97
+ context = viewmodel_class.new_serialize_context
98
+ data = ViewModel.serialize_to_hash([view], serialize_context: context)
99
+ refs = context.serialize_references_to_hash
100
+ [data, refs]
101
+ end
102
+
103
+ def parse_result(result)
104
+ data_json, refs_json = result
105
+ data = data_json.map { |d| JSON.parse(d) }
106
+ refs = refs_json.transform_values { |v| JSON.parse(v) }
107
+ [data, refs]
108
+ end
109
+
110
+ def fetch_with_cache
111
+ viewmodel_class.viewmodel_cache.fetch([root.id])
112
+ end
113
+
114
+ def serialize_with_cache
115
+ parse_result(fetch_with_cache)
116
+ end
117
+
118
+ module BehavesLikeACache
119
+ extend ActiveSupport::Concern
120
+ included do
121
+ it 'returns the right serialization' do
122
+ value(serialize_with_cache).must_equal(serialize_from_database)
123
+ end
124
+
125
+ it 'returns the right serialization after caching' do
126
+ fetch_with_cache
127
+ value(serialize_from_database).must_equal(serialize_with_cache)
128
+ end
129
+
130
+ it 'writes to the cache after fetching' do
131
+ cached_value = read_cache(viewmodel_class, root.id)
132
+ value(cached_value).wont_be(:present?)
133
+
134
+ fetch_with_cache
135
+
136
+ cached_value = read_cache(viewmodel_class, root.id)
137
+ value(cached_value).must_be(:present?)
138
+ end
139
+
140
+ it 'saves the returned serialization in the cache' do
141
+ data, refs = fetch_with_cache
142
+ value(data.size).must_equal(1)
143
+
144
+ cached_root = read_cache(viewmodel_class, root.id)
145
+ value(cached_root).must_be(:present?)
146
+ value(cached_root[:data]).must_equal(data.first)
147
+
148
+ ref_cache = cached_root[:ref_cache]
149
+ value(refs.size).must_equal(ref_cache.size)
150
+
151
+ refs.each do |key, ref_data|
152
+ view_name, id = ref_cache[key]
153
+ value(view_name).must_be(:present?)
154
+ value(id).must_be(:present?)
155
+
156
+ # The cached reference must correspond to the returned data.
157
+ parsed_data = JSON.parse(ref_data)
158
+ value(parsed_data["id"]).must_equal(id)
159
+ value(parsed_data["_type"]).must_equal(view_name)
160
+
161
+ # When the cached reference is to independently cached data
162
+ # (SharedView in this test), make sure that data is correctly
163
+ # cached.
164
+ next unless view_name == "Shared"
165
+ value(id).must_equal(shared.id)
166
+ cached_shared = read_cache(shared_viewmodel_class, id)
167
+ value(cached_shared).must_be(:present?)
168
+ value(cached_shared[:data]).must_equal(ref_data)
169
+ value(cached_shared[:ref_cache]).must_be(:blank?)
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ describe 'with owned and shared children' do
176
+ include CacheableParentAndChildren
177
+ include BehavesLikeACache
178
+
179
+ describe 'with a record in the cache' do
180
+ # Fetch the root record to ensure it's in the cache
181
+ before(:each) do
182
+ viewmodel_class.viewmodel_cache.fetch([root.id])
183
+ end
184
+
185
+ def change_in_database
186
+ root.update_attribute(:name, "CHANGEDROOT")
187
+ shared.update_attribute(:name, "CHANGEDSHARED")
188
+ end
189
+
190
+ it 'resolves from the cache' do
191
+ before_data, before_refs = serialize_from_database
192
+ change_in_database
193
+
194
+ cache_data, cache_refs = serialize_with_cache
195
+ value(cache_data).must_equal(before_data)
196
+ value(cache_refs).must_equal(before_refs)
197
+ end
198
+
199
+ it 'can clear the root cache' do
200
+ _before_data, before_refs = serialize_from_database
201
+ change_in_database
202
+ viewmodel_class.viewmodel_cache.clear
203
+
204
+ cache_data, cache_refs = serialize_with_cache
205
+ value(cache_data[0]["name"]).must_equal("CHANGEDROOT") # Root view invalidated
206
+ value(cache_refs).must_equal(before_refs) # Shared view not invalidated
207
+ end
208
+
209
+ describe 'when deserializing' do
210
+ it 'does not clear the cache on round-trip' do
211
+ alter_by_view!(viewmodel_class, root) {}
212
+
213
+ cached_root_value = read_cache(viewmodel_class, root.id)
214
+ value(cached_root_value).must_be(:present?)
215
+
216
+ cached_root_value = read_cache(shared_viewmodel_class, shared.id)
217
+ value(cached_root_value).must_be(:present?)
218
+ end
219
+
220
+ it 'clears only the root cache on edit to root' do
221
+ alter_by_view!(viewmodel_class, root) do |data, _refs|
222
+ data['name'] = 'new name'
223
+ end
224
+
225
+ cached_root_value = read_cache(viewmodel_class, root.id)
226
+ value(cached_root_value).wont_be(:present?)
227
+
228
+ cached_root_value = read_cache(shared_viewmodel_class, shared.id)
229
+ value(cached_root_value).must_be(:present?)
230
+ end
231
+
232
+ it 'clears only the root cache on edit to owned child' do
233
+ alter_by_view!(viewmodel_class, root) do |data, _refs|
234
+ data['child']['name'] = 'new child name'
235
+ end
236
+
237
+ cached_root_value = read_cache(viewmodel_class, root.id)
238
+ value(cached_root_value).wont_be(:present?)
239
+
240
+ cached_root_value = read_cache(shared_viewmodel_class, shared.id)
241
+ value(cached_root_value).must_be(:present?)
242
+ end
243
+
244
+ it 'clears only the shared child cache on edit to shared child' do
245
+ alter_by_view!(viewmodel_class, root) do |_data, refs|
246
+ refs.values.first['name'] = 'new shared name'
247
+ end
248
+
249
+ cached_root_value = read_cache(viewmodel_class, root.id)
250
+ value(cached_root_value).must_be(:present?)
251
+
252
+ cached_root_value = read_cache(shared_viewmodel_class, shared.id)
253
+ value(cached_root_value).wont_be(:present?)
254
+ end
255
+ end
256
+
257
+ it 'can delete an entity from a cache' do
258
+ _before_data, before_refs = serialize_from_database
259
+ change_in_database
260
+ viewmodel_class.viewmodel_cache.delete(root.id)
261
+
262
+ cache_data, cache_refs = serialize_with_cache
263
+ value(cache_data[0]["name"]).must_equal("CHANGEDROOT")
264
+ value(cache_refs).must_equal(before_refs)
265
+ end
266
+
267
+ it 'can clear a referenced cache' do
268
+ change_in_database
269
+ shared_viewmodel_class.viewmodel_cache.clear
270
+
271
+ # Shared view invalidated, but root view not
272
+ cache_data, cache_hrefs = serialize_with_cache
273
+ value(cache_data[0]["name"]).must_equal("root1")
274
+ value(cache_hrefs.values[0]["name"]).must_equal("CHANGEDSHARED")
275
+ end
276
+
277
+ it 'can clear a cache via its external cache group' do
278
+ change_in_database
279
+ shared_cache_group.invalidate_cache_group
280
+
281
+ # Shared view invalidated, but root view not
282
+ cache_data, cache_hrefs = serialize_with_cache
283
+ value(cache_data[0]["name"]).must_equal("root1")
284
+ value(cache_hrefs.values[0]["name"]).must_equal("CHANGEDSHARED")
285
+ end
286
+
287
+ describe 'and a record not in the cache' do
288
+ let(:root2) { model_class.create!(name: "root2", child: Child.new(name: "owned2"), shared: shared) }
289
+
290
+ def serialize_from_database
291
+ views = model_class.find(root.id, root2.id).map { |r| viewmodel_class.new(r) }
292
+ context = viewmodel_class.new_serialize_context
293
+ data = ViewModel.serialize_to_hash(views, serialize_context: context)
294
+ refs = context.serialize_references_to_hash
295
+ [data, refs]
296
+ end
297
+
298
+ def fetch_with_cache
299
+ viewmodel_class.viewmodel_cache.fetch([root.id, root2.id])
300
+ end
301
+
302
+ it 'merges matching shared references between cache hits and misses' do
303
+ db_data, db_refs = serialize_from_database
304
+ value(db_refs.size).must_equal(1)
305
+
306
+ cache_data, cache_refs = serialize_with_cache
307
+ value(cache_data).must_equal(db_data)
308
+ value(cache_refs).must_equal(db_refs)
309
+ end
310
+
311
+ it 'merges cache hits and misses' do
312
+ _, refs = serialize_from_database
313
+ change_in_database
314
+
315
+ cache_data, cache_refs = serialize_with_cache
316
+ value(cache_data[0]["name"]).must_equal("root1")
317
+ value(cache_data[1]["name"]).must_equal("root2")
318
+ value(cache_refs).must_equal(refs)
319
+ end
320
+ end
321
+ end
322
+ end
323
+
324
+ describe "with a non-cacheable shared child" do
325
+ include ViewModelSpecHelpers::ParentAndSharedChild
326
+ def model_attributes
327
+ super.merge(viewmodel: ->(_) { cacheable! })
328
+ end
329
+
330
+ let(:root) { model_class.create!(name: "root1", child: Child.new(name: "owned1")) }
331
+ let(:root_view) { viewmodel_class.new(root) }
332
+
333
+ include BehavesLikeACache
334
+ end
335
+
336
+ describe 'when fetched by viewmodel' do
337
+ def fetch_with_cache
338
+ viewmodel_class.viewmodel_cache.fetch_by_viewmodel([root_view])
339
+ end
340
+
341
+ include CacheableParentAndChildren
342
+ include BehavesLikeACache
343
+
344
+ it 'can handle duplicates' do
345
+ data, _refs = viewmodel_class.viewmodel_cache.fetch_by_viewmodel([root_view, root_view])
346
+ value(data.size).must_equal(2)
347
+ value(data[0]).must_equal(data[1])
348
+ end
349
+ end
350
+ end
351
+ end