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,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