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,80 @@
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::CounterTest < ActiveSupport::TestCase
9
+ include ARVMTestUtilities
10
+
11
+ def before_all
12
+ super
13
+
14
+ build_viewmodel(:Category) do
15
+ define_schema do |t|
16
+ t.string :name
17
+ t.integer :products_count, :null => false, :default => 0
18
+ end
19
+
20
+ define_model do
21
+ has_many :products, dependent: :destroy
22
+ end
23
+
24
+ define_viewmodel do
25
+ association :products
26
+ end
27
+ end
28
+
29
+ build_viewmodel(:Product) do
30
+ define_schema do |t|
31
+ t.string :name
32
+ t.references :category, :foreign_key => true
33
+ end
34
+
35
+ define_model do
36
+ belongs_to :category, counter_cache: true
37
+ end
38
+
39
+ define_viewmodel do
40
+ attribute :name
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ def setup
47
+ super
48
+ @category1 = Category.create(name: 'c1', products: [Product.new(name: 'p1')])
49
+ enable_logging!
50
+ end
51
+
52
+ def test_counter_cache_create
53
+ alter_by_view!(CategoryView, @category1) do |view, refs|
54
+ view['products'] << {'_type' => 'Product'}
55
+ end
56
+ assert_equal(2, @category1.products_count)
57
+ end
58
+
59
+ def test_counter_cache_move
60
+ @category2 = Category.create(name: 'c2')
61
+ alter_by_view!(CategoryView, [@category1, @category2]) do |(c1view, c2view), refs|
62
+ c2view['products'] = c1view['products']
63
+ c1view['products'] = []
64
+ end
65
+ assert_equal(0, @category1.products_count)
66
+ assert_equal(1, @category2.products_count)
67
+ end
68
+
69
+ def test_counter_cache_delete
70
+ alter_by_view!(CategoryView, @category1) do |view, refs|
71
+ view['products'] = []
72
+ end
73
+ assert_equal(0, @category1.products_count)
74
+ end
75
+
76
+ def test_counter_culture_wat
77
+ cat = Category.create(name: 'c1', products: [Product.new(name: 'p1')])
78
+ assert_equal(1, cat.products_count)
79
+ end
80
+ end
@@ -0,0 +1,388 @@
1
+ # coding: utf-8
2
+ require_relative "../../../helpers/arvm_test_utilities.rb"
3
+ require_relative "../../../helpers/arvm_test_models.rb"
4
+
5
+ require "minitest/autorun"
6
+
7
+ require "view_model/active_record"
8
+
9
+
10
+ require 'renum'
11
+
12
+ class ViewModel::ActiveRecord::SpecializeAssociationTest < ActiveSupport::TestCase
13
+ include ARVMTestUtilities
14
+
15
+ def before_all
16
+ super
17
+
18
+ build_viewmodel(:Text) do
19
+ define_schema do |t|
20
+ t.string :text
21
+ end
22
+
23
+ define_model do
24
+ has_many :translations, dependent: :destroy, inverse_of: :text
25
+ end
26
+
27
+ define_viewmodel do
28
+ attributes :text
29
+ association :translations
30
+
31
+ def self.pre_parse_translations(viewmodel_reference, metadata, hash, translations)
32
+ raise "type check" unless translations.is_a?(Hash) && translations.all? { |k, v| k.is_a?(String) && v.is_a?(String) }
33
+ hash["translations"] = translations.map { |lang, text| { "_type" => "Translation", "language" => lang, "translation" => text } }
34
+ end
35
+
36
+ def resolve_translations(update_datas, previous_translation_views)
37
+ existing = previous_translation_views.index_by { |x| [x.model.language, x.model.translation] }
38
+ update_datas.map do |update_data|
39
+ existing.fetch([update_data["language"], update_data["translation"]]) { TranslationView.for_new_model }
40
+ end
41
+ end
42
+
43
+ def serialize_translations(json, serialize_context:)
44
+ translation_views = self.translations
45
+ json.translations do
46
+ translation_views.each do |tv|
47
+ json.set!(tv.language, tv.translation)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ build_viewmodel(:translation) do
55
+ define_schema do |t|
56
+ t.references :text
57
+ t.string :language
58
+ t.string :translation
59
+ end
60
+
61
+ define_model do
62
+ belongs_to :text, inverse_of: :translations
63
+ end
64
+
65
+ define_viewmodel do
66
+ attributes :language, :translation
67
+ end
68
+ end
69
+ end
70
+
71
+ def setup
72
+ super
73
+
74
+ @text1 = Text.create(text: "dog",
75
+ translations: [Translation.new(language: "ja", translation: "犬"),
76
+ Translation.new(language: "fr", translation: "chien")])
77
+
78
+ @text1_view = {
79
+ "id" => @text1.id,
80
+ "_type" => "Text",
81
+ "_version" => 1,
82
+ "text" => "dog",
83
+ "translations" => {
84
+ "ja" => "犬",
85
+ "fr" => "chien"
86
+ }
87
+ }
88
+
89
+ enable_logging!
90
+ end
91
+
92
+ def test_serialize
93
+ assert_equal(@text1_view, serialize(TextView.new(@text1)))
94
+ end
95
+
96
+ def test_create
97
+ create_view = @text1_view.dup.tap {|v| v.delete('id')}
98
+ new_text_view = TextView.deserialize_from_view(create_view)
99
+ new_text_model = new_text_view.model
100
+
101
+ assert_equal('dog', new_text_model.text)
102
+
103
+ new_translations = new_text_model.translations.map do |x|
104
+ [x['language'], x['translation']]
105
+ end
106
+ assert_equal([%w(fr chien),
107
+ %w(ja 犬)],
108
+ new_translations.sort)
109
+ end
110
+
111
+ def test_noop
112
+ original_translation_models = @text1.translations.order(:id).to_a
113
+ alter_by_view!(TextView, @text1) {}
114
+ assert_equal(original_translation_models, @text1.translations.order(:id).to_a)
115
+ end
116
+ end
117
+
118
+ class ViewModel::ActiveRecord::FlattenAssociationTest < ActiveSupport::TestCase
119
+ include ARVMTestUtilities
120
+
121
+ def before_all
122
+ super
123
+ self.class.build_viewmodels(self)
124
+ end
125
+
126
+ def self.build_viewmodels(instance, in_collection: false)
127
+ instance.build_viewmodel(:QuizSection) do
128
+ define_schema do |t|
129
+ t.string :quiz_name
130
+ end
131
+ define_model do
132
+ has_one :section, as: :section_data
133
+ end
134
+ define_viewmodel do
135
+ attributes :quiz_name
136
+ end
137
+ end
138
+
139
+ instance.build_viewmodel(:VocabSection) do
140
+ define_schema do |t|
141
+ t.string :vocab_word
142
+ end
143
+ define_model do
144
+ has_one :section, as: :section_data
145
+ end
146
+ define_viewmodel do
147
+ attributes :vocab_word
148
+ end
149
+ end
150
+
151
+ # define a `renum` enumeration
152
+ Object.enum :SectionType do
153
+ Simple(nil)
154
+ Quiz(QuizSectionView)
155
+ Vocab(VocabSectionView)
156
+
157
+ attr_reader :viewmodel
158
+
159
+ def init(viewmodel)
160
+ @viewmodel = viewmodel
161
+ end
162
+
163
+ def construct_hash(members)
164
+ case self
165
+ when SectionType::Simple
166
+ raise "nopes" if members.present?
167
+ nil
168
+ else
169
+ members.merge("_type" => viewmodel.view_name)
170
+ end
171
+ end
172
+
173
+ def self.with_name(name)
174
+ if name.nil?
175
+ SectionType::Simple
176
+ else
177
+ super
178
+ end
179
+ end
180
+
181
+ def self.for_viewmodel(viewmodel)
182
+ @vm_index ||= SectionType.values.index_by(&:viewmodel)
183
+ vm_class = viewmodel.try(:class)
184
+ @vm_index.fetch(vm_class)
185
+ end
186
+ end
187
+
188
+ instance.define_singleton_method(:after_all) do
189
+ Object.send(:remove_const, :SectionType)
190
+ super()
191
+ end
192
+
193
+ instance.build_viewmodel(:Section) do
194
+ define_schema do |t|
195
+ t.string :name
196
+ t.references :section_data
197
+ t.string :section_data_type
198
+
199
+ if in_collection
200
+ t.references in_collection
201
+ end
202
+ end
203
+
204
+ define_model do
205
+ belongs_to :section_data, polymorphic: true, dependent: :destroy
206
+
207
+ if in_collection
208
+ belongs_to in_collection, inverse_of: :sections
209
+ end
210
+ end
211
+
212
+ define_viewmodel do
213
+ attributes :name
214
+ association :section_data, viewmodels: [VocabSectionView, QuizSectionView]
215
+
216
+ def self.pre_parse(_viewmodel_reference, _metadata, user_data)
217
+ section_type_name = user_data.delete('section_type')
218
+
219
+ section_type = SectionType.with_name(section_type_name)
220
+ raise "Invalid section type: #{section_type_name.inspect}" unless section_type
221
+
222
+ user_data["section_data"] = section_type.construct_hash(user_data.slice!(*self._members.keys))
223
+ end
224
+
225
+ def resolve_section_data(update_data, previous_translation_view)
226
+ # Reuse if it's the same type
227
+ if update_data.viewmodel_class == previous_translation_view.class
228
+ previous_translation_view
229
+ else
230
+ update_data.viewmodel_class.for_new_model
231
+ end
232
+ end
233
+
234
+ def serialize_section_data(json, serialize_context:)
235
+ sd_view = self.section_data
236
+ section_type = SectionType.for_viewmodel(sd_view)
237
+
238
+ json.section_type section_type.name
239
+ if sd_view
240
+ sd_view.serialize_members(json, serialize_context: serialize_context)
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ end
247
+
248
+ def setup
249
+ super
250
+
251
+ @simplesection = Section.create(name: "simple1")
252
+ @simplesection_view = {
253
+ "id" => @simplesection.id,
254
+ "_type" => "Section",
255
+ "_version" => 1,
256
+ "section_type" => "Simple",
257
+ "name" => "simple1"
258
+ }
259
+
260
+ @quizsection = Section.create(name: "quiz1", section_data: QuizSection.new(quiz_name: "qq"))
261
+ @quizsection_view = {
262
+ "id" => @quizsection.id,
263
+ "_type" => "Section",
264
+ "_version" => 1,
265
+ "section_type" => "Quiz",
266
+ "name" => "quiz1",
267
+ "quiz_name" => "qq"
268
+ }
269
+
270
+ @vocabsection = Section.create(name: "vocab1", section_data: VocabSection.new(vocab_word: "dog"))
271
+ @vocabsection_view = {
272
+ "id" => @vocabsection.id,
273
+ "_type" => "Section",
274
+ "_version" => 1,
275
+ "section_type" => "Vocab",
276
+ "name" => "vocab1",
277
+ "vocab_word" => "dog"
278
+ }
279
+
280
+ enable_logging!
281
+ end
282
+
283
+ def test_serialize
284
+ v = SectionView.new(@simplesection)
285
+ assert_equal(@simplesection_view, v.to_hash)
286
+
287
+ v = SectionView.new(@quizsection)
288
+ assert_equal(@quizsection_view, v.to_hash)
289
+
290
+ v = SectionView.new(@vocabsection)
291
+ assert_equal(@vocabsection_view, v.to_hash)
292
+ end
293
+
294
+ def new_view_like(view)
295
+ view.dup.tap { |v| v.delete('id') }
296
+ end
297
+
298
+ def test_create
299
+ assert_section = ->(model, name, &check_section){
300
+ assert(!model.changed?)
301
+ assert(!model.new_record?)
302
+ assert_equal(name, model.name)
303
+
304
+ sd = model.section_data
305
+ if check_section
306
+ assert(sd)
307
+ assert(!sd.changed?)
308
+ assert(!sd.new_record?)
309
+ check_section.call(sd)
310
+ else
311
+ assert_nil(sd)
312
+ end
313
+ }
314
+
315
+ v = SectionView.deserialize_from_view(new_view_like(@simplesection_view))
316
+ assert_section.call(v.model, "simple1")
317
+
318
+ v = SectionView.deserialize_from_view(new_view_like(@quizsection_view))
319
+ assert_section.call(v.model, "quiz1") do |m|
320
+ assert(m.is_a?(QuizSection))
321
+ assert_equal("qq", m.quiz_name)
322
+ end
323
+
324
+ v = SectionView.deserialize_from_view(new_view_like(@vocabsection_view))
325
+ assert_section.call(v.model, "vocab1") do |m|
326
+ assert(m.is_a?(VocabSection))
327
+ assert_equal("dog", m.vocab_word)
328
+ end
329
+ end
330
+
331
+ def test_noop
332
+ # Simple sections have no stability worth checking
333
+
334
+ old_quizsection_data = @quizsection.section_data
335
+ alter_by_view!(SectionView, @quizsection) {}
336
+ assert_equal(old_quizsection_data, @quizsection.section_data)
337
+
338
+ old_vocabsection_data = @vocabsection.section_data
339
+ alter_by_view!(SectionView, @vocabsection) {}
340
+ assert_equal(old_vocabsection_data, @vocabsection.section_data)
341
+ end
342
+
343
+ class InCollectionTest < ActiveSupport::TestCase
344
+ include ARVMTestUtilities
345
+
346
+ def before_all
347
+ super
348
+ build_viewmodel(:Exercise) do
349
+ define_schema do |t|
350
+ t.string :name
351
+ end
352
+ define_model do
353
+ has_many :sections
354
+ end
355
+ define_viewmodel do
356
+ attribute :name
357
+ association :sections
358
+ end
359
+ end
360
+ ViewModel::ActiveRecord::FlattenAssociationTest.build_viewmodels(self, in_collection: :exercise)
361
+ end
362
+
363
+ def setup
364
+ super
365
+ sections = [
366
+ Section.new(name: "simple1"),
367
+ Section.new(name: "quiz1", section_data: QuizSection.new(quiz_name: "qq")),
368
+ Section.new(name: "vocab1", section_data: VocabSection.new(vocab_word: "dog")),
369
+ ]
370
+ @exercise1 = Exercise.create(sections: sections)
371
+ end
372
+
373
+ def test_functional_update
374
+ alter_by_view!(ExerciseView, @exercise1) do |view, refs|
375
+ view['sections'] = {
376
+ '_type' => '_update',
377
+ 'actions' => [{ '_type' => 'append',
378
+ 'values' => [{ '_type' => 'Section',
379
+ 'section_type' => 'Vocab',
380
+ 'name' => 'vocab_new',
381
+ 'vocab_word' => 'cat',
382
+ }],
383
+ }]
384
+ }
385
+ end
386
+ end
387
+ end
388
+ end