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