iknow_view_models 2.8.4

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.
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,334 @@
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::HasOneTest < ActiveSupport::TestCase
9
+ include ARVMTestUtilities
10
+
11
+ def self.build_target(arvm_test_case)
12
+ arvm_test_case.build_viewmodel(:Target) do
13
+ define_schema do |t|
14
+ t.string :text
15
+ t.references :parent, foreign_key: true
16
+ end
17
+
18
+ define_model do
19
+ belongs_to :parent, inverse_of: :target
20
+ end
21
+
22
+ define_viewmodel do
23
+ attributes :text
24
+ end
25
+ end
26
+ end
27
+
28
+
29
+ def self.build_parent(arvm_test_case)
30
+ arvm_test_case.build_viewmodel(:Parent) do
31
+ define_schema do |t|
32
+ t.string :name
33
+ end
34
+
35
+ define_model do
36
+ has_one :target, dependent: :destroy, inverse_of: :parent
37
+ end
38
+
39
+ define_viewmodel do
40
+ attributes :name
41
+ associations :target
42
+ end
43
+ end
44
+ end
45
+
46
+ def before_all
47
+ super
48
+
49
+ self.class.build_parent(self)
50
+ self.class.build_target(self)
51
+ end
52
+
53
+ def setup
54
+ super
55
+
56
+ # TODO make a `has_list?` that allows a parent to set all children as an array
57
+ @parent1 = Parent.new(name: "p1",
58
+ target: Target.new(text: "p1t"))
59
+ @parent1.save!
60
+
61
+ @parent2 = Parent.new(name: "p2",
62
+ target: Target.new(text: "p2t"))
63
+
64
+ @parent2.save!
65
+
66
+ enable_logging!
67
+ end
68
+
69
+ def test_loading_batching
70
+ log_queries do
71
+ serialize(ParentView.load)
72
+ end
73
+ assert_equal(['Parent Load', 'Target Load'],
74
+ logged_load_queries)
75
+ end
76
+
77
+ def test_create_from_view
78
+ view = {
79
+ "_type" => "Parent",
80
+ "name" => "p",
81
+ "target" => { "_type" => "Target", "text" => "t" },
82
+ }
83
+
84
+ pv = ParentView.deserialize_from_view(view)
85
+ p = pv.model
86
+
87
+ assert(!p.changed?)
88
+ assert(!p.new_record?)
89
+
90
+ assert_equal("p", p.name)
91
+
92
+
93
+ assert(p.target.present?)
94
+ assert_equal("t", p.target.text)
95
+ end
96
+
97
+ def test_serialize_view
98
+ view, _refs = serialize_with_references(ParentView.new(@parent1))
99
+ assert_equal({ "_type" => "Parent",
100
+ "_version" => 1,
101
+ "id" => @parent1.id,
102
+ "name" => @parent1.name,
103
+ "target" => { "_type" => "Target",
104
+ "_version" => 1,
105
+ "id" => @parent1.target.id,
106
+ "text" => @parent1.target.text } },
107
+ view)
108
+ end
109
+
110
+ def test_swap_has_one
111
+ @parent1.update(target: t1 = Target.new)
112
+ @parent2.update(target: t2 = Target.new)
113
+
114
+ deserialize_context = ViewModelBase.new_deserialize_context
115
+
116
+ ParentView.deserialize_from_view(
117
+ [update_hash_for(ParentView, @parent1) { |p| p['target'] = update_hash_for(TargetView, t2) },
118
+ update_hash_for(ParentView, @parent2) { |p| p['target'] = update_hash_for(TargetView, t1) }],
119
+ deserialize_context: deserialize_context)
120
+
121
+ assert_equal(Set.new([ViewModel::Reference.new(ParentView, @parent1.id),
122
+ ViewModel::Reference.new(ParentView, @parent2.id)]),
123
+ deserialize_context.valid_edit_refs.to_set)
124
+
125
+ @parent1.reload
126
+ @parent2.reload
127
+
128
+ assert_equal(@parent1.target, t2)
129
+ assert_equal(@parent2.target, t1)
130
+ end
131
+
132
+ def test_has_one_create_nil
133
+ view = { '_type' => 'Parent', 'name' => 'p', 'target' => nil }
134
+ pv = ParentView.deserialize_from_view(view)
135
+ assert_nil(pv.model.target)
136
+ end
137
+
138
+ def test_has_one_create
139
+ @parent1.update(target: nil)
140
+
141
+ alter_by_view!(ParentView, @parent1) do |view, refs|
142
+ view['target'] = { '_type' => 'Target', 'text' => 't' }
143
+ end
144
+
145
+ assert_equal('t', @parent1.target.text)
146
+ end
147
+
148
+ def test_has_one_update
149
+ alter_by_view!(ParentView, @parent1) do |view, refs|
150
+ view['target']['text'] = "hello"
151
+ end
152
+
153
+ assert_equal('hello', @parent1.target.text)
154
+ end
155
+
156
+ def test_has_one_destroy
157
+ old_target = @parent1.target
158
+ alter_by_view!(ParentView, @parent1) do |view, refs|
159
+ view['target'] = nil
160
+ end
161
+ assert(Target.where(id: old_target.id).blank?)
162
+ end
163
+
164
+ def test_has_one_move_and_replace
165
+ old_parent1_target = @parent1.target
166
+ old_parent2_target = @parent2.target
167
+
168
+ alter_by_view!(ParentView, [@parent1, @parent2]) do |(p1, p2), refs|
169
+ p2['target'] = p1['target']
170
+ p1['target'] = nil
171
+ end
172
+
173
+ assert(@parent1.target.blank?)
174
+ assert_equal(old_parent1_target, @parent2.target)
175
+ assert(Target.where(id: old_parent2_target).blank?)
176
+ end
177
+
178
+ def test_has_one_cannot_duplicate_unreleased_child
179
+ # p2 shouldn't be able to copy p1's target
180
+ assert_raises(ViewModel::DeserializationError::DuplicateNodes) do
181
+ alter_by_view!(ParentView, [@parent1, @parent2]) do |(p1, p2), _refs|
182
+ p2['target'] = p1['target'].dup
183
+ end
184
+ end
185
+ end
186
+
187
+ def test_has_one_cannot_duplicate_implicitly_unreleased_child
188
+ # p2 shouldn't be able to copy p1's target, even when p1 doesn't explicitly
189
+ # specify the association
190
+ assert_raises(ViewModel::DeserializationError::ParentNotFound) do
191
+ alter_by_view!(ParentView, [@parent1, @parent2]) do |(p1, p2), _refs|
192
+ p2['target'] = p1['target']
193
+ p1.delete('target')
194
+ end
195
+ end
196
+ end
197
+
198
+ def test_has_one_cannot_take_from_outside_tree
199
+ t3 = Parent.create(target: Target.new(text: 'hi')).target
200
+
201
+ assert_raises(ViewModel::DeserializationError::ParentNotFound) do
202
+ alter_by_view!(ParentView, [@parent1]) do |(p1), _refs|
203
+ p1['target'] = update_hash_for(TargetView, t3)
204
+ end
205
+ end
206
+ end
207
+
208
+ def test_has_one_cannot_take_unparented_from_outside_tree
209
+ t3 = Target.create(text: 'hi') # no parent
210
+
211
+ assert_raises(ViewModel::DeserializationError::ParentNotFound) do
212
+ alter_by_view!(ParentView, @parent1) do |p1, _refs|
213
+ p1['target'] = update_hash_for(TargetView, t3)
214
+ end
215
+ end
216
+ end
217
+
218
+ def test_bad_single_association
219
+ view = {
220
+ "_type" => "Parent",
221
+ "target" => []
222
+ }
223
+ ex = assert_raises(ViewModel::DeserializationError::InvalidSyntax) do
224
+ ParentView.deserialize_from_view(view)
225
+ end
226
+ assert_match(/not an object/, ex.message)
227
+ end
228
+
229
+
230
+ class RenameTest < ActiveSupport::TestCase
231
+ include ARVMTestUtilities
232
+
233
+ def before_all
234
+ super
235
+
236
+ build_viewmodel(:Parent) do
237
+ define_schema do |t|
238
+ t.string :name
239
+ end
240
+
241
+ define_model do
242
+ has_one :target, dependent: :destroy, inverse_of: :parent
243
+ end
244
+
245
+ define_viewmodel do
246
+ attributes :name
247
+ association :target, as: :something_else
248
+ end
249
+ end
250
+
251
+ ViewModel::ActiveRecord::HasOneTest.build_target(self)
252
+ end
253
+
254
+ def setup
255
+ super
256
+
257
+ @parent = Parent.create(target: Target.new(text: 'target text'))
258
+
259
+ enable_logging!
260
+ end
261
+
262
+ def test_dependencies
263
+ root_updates, _ref_updates = ViewModel::ActiveRecord::UpdateData.parse_hashes([{ '_type' => 'Parent', 'something_else' => nil }])
264
+ assert_equal(DeepPreloader::Spec.new('target' => DeepPreloader::Spec.new), root_updates.first.preload_dependencies)
265
+ assert_equal({ 'something_else' => {} }, root_updates.first.updated_associations)
266
+ end
267
+
268
+ def test_renamed_roundtrip
269
+ alter_by_view!(ParentView, @parent) do |view, refs|
270
+ assert_equal({ 'id' => @parent.target.id,
271
+ '_type' => 'Target',
272
+ '_version' => 1,
273
+ 'text' => 'target text' },
274
+ view['something_else'])
275
+ view['something_else']['text'] = 'target new text'
276
+ end
277
+
278
+ assert_equal('target new text', @parent.target.text)
279
+ end
280
+ end
281
+
282
+ class FreedChildrenTest < ActiveSupport::TestCase
283
+ include ARVMTestUtilities
284
+
285
+ def before_all
286
+ build_viewmodel(:Aye) do
287
+ define_schema do |t|
288
+ t.references :bee
289
+ end
290
+ define_model do
291
+ belongs_to :bee, inverse_of: :aye, dependent: :destroy
292
+ end
293
+ define_viewmodel do
294
+ association :bee
295
+ end
296
+ end
297
+
298
+ build_viewmodel(:Bee) do
299
+ define_schema do |t|
300
+ end
301
+ define_model do
302
+ has_one :aye, inverse_of: :bee
303
+ has_one :cee, inverse_of: :bee, dependent: :destroy
304
+ end
305
+ define_viewmodel do
306
+ association :cee
307
+ end
308
+ end
309
+
310
+ build_viewmodel(:Cee) do
311
+ define_schema do |t|
312
+ t.references :bee
313
+ end
314
+ define_model do
315
+ belongs_to :bee, inverse_of: :cee
316
+ end
317
+ define_viewmodel do
318
+ end
319
+ end
320
+ end
321
+
322
+ def test_reclaim_grandchild_from_deleted_child
323
+ skip 'Issue #8'
324
+
325
+ model = Aye.create(bee: Bee.new(cee: Cee.new))
326
+
327
+ # This test currently fails because we only release the top of the deleted
328
+ # subtree to the release pool, and so its children cannot be reclaimed.
329
+ alter_by_view!(AyeView, model) do |view, _refs|
330
+ view['bee'].delete('id')
331
+ end
332
+ end
333
+ end
334
+ end
@@ -0,0 +1,75 @@
1
+ require "minitest/autorun"
2
+ require "minitest/unit"
3
+ require "minitest/hooks"
4
+
5
+ require_relative "../../../helpers/arvm_test_utilities.rb"
6
+ require_relative "../../../helpers/arvm_test_models.rb"
7
+ require_relative "../../../helpers/viewmodel_spec_helpers.rb"
8
+
9
+ require "view_model"
10
+ require "view_model/active_record"
11
+
12
+ module NSTest
13
+ end
14
+
15
+ class ViewModel::ActiveRecord::NamespacingTest < ActiveSupport::TestCase
16
+ include ViewModelSpecHelpers::ParentAndHasOneChild
17
+ extend Minitest::Spec::DSL
18
+
19
+ def namespace
20
+ NSTest
21
+ end
22
+
23
+ def model_attributes
24
+ parent_attrs = super
25
+
26
+ ViewModel::TestHelpers::ARVMBuilder::Spec.new(
27
+ schema: parent_attrs.schema,
28
+ viewmodel: parent_attrs.viewmodel,
29
+ model: ->(_) {
30
+ has_one :child, inverse_of: :model, class_name: "NSTest::Child", dependent: :destroy
31
+ })
32
+ end
33
+
34
+ describe 'inference' do
35
+ it "assigns a transformed view name from a namespaced class" do
36
+ assert_equal("NSTest.Model", viewmodel_class.view_name)
37
+ end
38
+
39
+ it "can look up a viewmodel by inference from an association to a namespaced model" do
40
+ child_viewmodel_class # test depends on child_viewmodel_class
41
+
42
+ assert_equal(viewmodel_class._association_data('child').viewmodel_class,
43
+ child_viewmodel_class)
44
+ end
45
+
46
+ it "can infer the model class from a namespaced view class name" do
47
+ assert_equal(viewmodel_class.model_class, model_class)
48
+ end
49
+ end
50
+
51
+ describe 'access control' do
52
+ include ARVMTestUtilities
53
+
54
+ it 'can apply access control policy for namespaced classes' do
55
+ _viewmodel_class = viewmodel_class
56
+
57
+ access_control_class =
58
+ Class.new(ViewModel::AccessControl::Tree) do
59
+ view(_viewmodel_class.view_name) do
60
+ visible_unless!("VETO-ERROR-MESSAGE") { true }
61
+ end
62
+ end
63
+
64
+ child_viewmodel_class # test depends on child_viewmodel_class
65
+
66
+ serialize_context = viewmodel_class.new_serialize_context(
67
+ access_control: access_control_class.new)
68
+
69
+ refute_serializes(viewmodel_class,
70
+ model_class.create!,
71
+ "VETO-ERROR-MESSAGE",
72
+ serialize_context: serialize_context)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,58 @@
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::AttributeViewTest < ActiveSupport::TestCase
9
+ include ARVMTestUtilities
10
+
11
+ def before_all
12
+ super
13
+
14
+ build_viewmodel(:Thing) do
15
+ define_schema do |t|
16
+ t.integer :a
17
+ t.integer :b
18
+ end
19
+
20
+ define_model do
21
+ end
22
+
23
+ define_viewmodel do
24
+ attribute :a
25
+ attribute :b, optional: true
26
+ end
27
+ end
28
+ end
29
+
30
+ def setup
31
+ super
32
+ @thing = Thing.create!(a: 1, b: 2)
33
+
34
+ @skel = { "_type" => "Thing",
35
+ "_version" => 1,
36
+ "id" => @thing.id }
37
+ end
38
+
39
+ def test_optional_not_serialized
40
+ view, _refs = serialize_with_references(ThingView.new(@thing))
41
+
42
+ assert_equal(@skel.merge("a" => 1), view)
43
+ end
44
+
45
+ def test_optional_included
46
+ view, _refs = serialize_with_references(ThingView.new(@thing),
47
+ serialize_context: ThingView.new_serialize_context(include: :b))
48
+
49
+ assert_equal(@skel.merge("a" => 1, "b" => 2), view)
50
+ end
51
+
52
+ def test_pruned_not_included
53
+ view, _refs = serialize_with_references(ThingView.new(@thing),
54
+ serialize_context: ThingView.new_serialize_context(include: :b, prune: :a))
55
+
56
+ assert_equal(@skel.merge("b" => 2), view)
57
+ end
58
+ end