iknow_view_models 3.2.8 → 3.2.13

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6955b0705dee2437a934217bad89cc3471bc947a17f7d5b082a1b536ac492dd
4
- data.tar.gz: bb831ef743c2ac4f4cc5a0400e41dc9ea14ca71f8994e3e6a0f8f94b48a30035
3
+ metadata.gz: 3991314cdb010347b417869ff3e2e22b0e7adb4ab590e25739cb6b12bf49a219
4
+ data.tar.gz: 0cd5994d07bdd9b9d311732be56b01a51d7b4f1eb71f7fa1d0217bf71911b97f
5
5
  SHA512:
6
- metadata.gz: ba2b82a74b00f528a9bbc8aca178ea22daf2689f243195586632ed4a82497e42b61924ac44603f67fa79caf5b1ae1841902f7fe9897f5be0c8d6e3cef77291e4
7
- data.tar.gz: 56fa447075a0cb6bc401eb70b871ee0d72aae29e938a14aaaecedc17e0ea7c6fcecd1dea71f5b3058b19ae83fe796a6a2e21505a892bdb2fa3304281ff774542
6
+ metadata.gz: a5cd70dd6f79db33a97dbdfb05eefec460c7e332784839ad461cf5a359307ce8abedb39c046d42d04a3b7355280d1ad54948cb6cc61fdc35effa0f684ea4c821
7
+ data.tar.gz: 6a673b6387892588434f981b57f8131e9c7a97991cc16d3901e72f043b9429efd0fef7a8e1f44cb0ea1ee364256d63d48f198a10e270b487d3da61311780eec7
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IknowViewModels
4
- VERSION = '3.2.8'
4
+ VERSION = '3.2.13'
5
5
  end
data/lib/view_model.rb CHANGED
@@ -377,3 +377,4 @@ require 'view_model/deserialize_context'
377
377
  require 'view_model/changes'
378
378
  require 'view_model/schemas'
379
379
  require 'view_model/error_view'
380
+ require 'view_model/garbage_collection'
@@ -182,9 +182,14 @@ class ViewModel::ActiveRecord < ViewModel::Record
182
182
  end
183
183
 
184
184
  # Constructs a preload specification of the required models for
185
- # serializing/deserializing this view.
186
- def eager_includes(serialize_context: new_serialize_context, include_referenced: true)
185
+ # serializing/deserializing this view. Cycles in the schema will be broken
186
+ # after two layers of eager loading.
187
+ def eager_includes(serialize_context: new_serialize_context, include_referenced: true, vm_path: [])
187
188
  association_specs = {}
189
+
190
+ return nil if vm_path.count(self) > 2
191
+
192
+ child_path = vm_path + [self]
188
193
  _members.each do |assoc_name, association_data|
189
194
  next unless association_data.is_a?(AssociationData)
190
195
  next if association_data.external?
@@ -201,7 +206,7 @@ class ViewModel::ActiveRecord < ViewModel::Record
201
206
  case
202
207
  when association_data.through?
203
208
  viewmodel = association_data.direct_viewmodel
204
- children = viewmodel.eager_includes(serialize_context: child_context, include_referenced: include_referenced)
209
+ children = viewmodel.eager_includes(serialize_context: child_context, include_referenced: include_referenced, vm_path: child_path)
205
210
 
206
211
  when !include_referenced && association_data.referenced?
207
212
  children = nil # Load up to the root viewmodel, but no further
@@ -210,13 +215,13 @@ class ViewModel::ActiveRecord < ViewModel::Record
210
215
  children_by_klass = {}
211
216
  association_data.viewmodel_classes.each do |vm_class|
212
217
  klass = vm_class.model_class.name
213
- children_by_klass[klass] = vm_class.eager_includes(serialize_context: child_context, include_referenced: include_referenced)
218
+ children_by_klass[klass] = vm_class.eager_includes(serialize_context: child_context, include_referenced: include_referenced, vm_path: child_path)
214
219
  end
215
220
  children = DeepPreloader::PolymorphicSpec.new(children_by_klass)
216
221
 
217
222
  else
218
223
  viewmodel = association_data.viewmodel_class
219
- children = viewmodel.eager_includes(serialize_context: child_context, include_referenced: include_referenced)
224
+ children = viewmodel.eager_includes(serialize_context: child_context, include_referenced: include_referenced, vm_path: child_path)
220
225
  end
221
226
 
222
227
  association_specs[association_data.direct_reflection.name.to_s] = children
@@ -340,12 +345,15 @@ class ViewModel::ActiveRecord < ViewModel::Record
340
345
 
341
346
  case
342
347
  when association_data.through?
343
- # associated here are join_table models; we need to get the far side out
348
+ # associated here are join-table models; we need to get the far side out
349
+ join_models = associated
350
+
344
351
  if association_data.direct_viewmodel._list_member?
345
- associated.order(association_data.direct_viewmodel._list_attribute_name)
352
+ attr = association_data.direct_viewmodel._list_attribute_name
353
+ join_models = join_models.sort_by { |j| j[attr] }
346
354
  end
347
355
 
348
- associated.map do |through_model|
356
+ join_models.map do |through_model|
349
357
  model = through_model.public_send(association_data.indirect_reflection.name)
350
358
  association_data.viewmodel_class_for_model!(model.class).new(model)
351
359
  end
@@ -177,28 +177,27 @@ class ViewModel::ActiveRecord::Cache
177
177
  ViewModel.serialize(viewmodel, json, serialize_context: serialize_context)
178
178
  end
179
179
 
180
+ # viewmodels referenced from roots
180
181
  referenced_viewmodels = serialize_context.extract_referenced_views!
181
182
 
182
183
  if migration_versions.present?
183
184
  migrator = ViewModel::DownMigrator.new(migration_versions)
184
185
 
185
- # This migration isn't given the chance to inspect/alter the contents
186
- # of referenced views, only their presence: it's strictly less
187
- # powerful than migrations on a fully serialized tree, as the only
188
- # possible action on a referenced child is to delete it. The effect of
189
- # this is that for sufficiently complex migrations where a parent view
190
- # must introduce children or alter the contents of its referenced
191
- # children, we may have to avoid caching while the migration is in
192
- # use.
186
+ # This migration isn't able to affect the contents of referenced
187
+ # views, only their presence. The references will be themselves
188
+ # rendered (and migrated) independently later. We mark the dummy
189
+ # references provided to exclude their partial contents from being
190
+ # themselves migrated.
193
191
  dummy_references = referenced_viewmodels.transform_values do |ref_vm|
194
192
  {
195
193
  ViewModel::TYPE_ATTRIBUTE => ref_vm.class.view_name,
196
194
  ViewModel::VERSION_ATTRIBUTE => ref_vm.class.schema_version,
197
195
  ViewModel::ID_ATTRIBUTE => ref_vm.id,
196
+ ViewModel::Migrator::EXCLUDE_FROM_MIGRATION => true,
198
197
  }.freeze
199
198
  end
200
199
 
201
- migrator.migrate!(builder.attributes!, references: dummy_references)
200
+ migrator.migrate!({ 'data' => builder.attributes!, 'references' => dummy_references })
202
201
 
203
202
  # Removed dummy references can be removed from referenced_viewmodels.
204
203
  referenced_viewmodels.keep_if { |k, _| dummy_references.has_key?(k) }
@@ -71,7 +71,7 @@ module ViewModel::ActiveRecord::Controller
71
71
  super.tap do |update_hash, refs|
72
72
  if migration_versions.present?
73
73
  migrator = ViewModel::UpMigrator.new(migration_versions)
74
- migrator.migrate!([update_hash, refs], references: refs)
74
+ migrator.migrate!({ 'data' => update_hash, 'references' => refs })
75
75
  end
76
76
  end
77
77
  end
@@ -84,7 +84,7 @@ module ViewModel::ActiveRecord::Controller
84
84
  if migration_versions.present?
85
85
  tree = jbuilder.attributes!
86
86
  migrator = ViewModel::DownMigrator.new(migration_versions)
87
- migrator.migrate!(tree, references: tree['references'])
87
+ migrator.migrate!(tree)
88
88
  end
89
89
  end
90
90
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ViewModel::GarbageCollection
4
+ class << self
5
+ def garbage_collect_references!(serialization)
6
+ return unless serialization.has_key?('references')
7
+
8
+ roots = serialization.except('references')
9
+ references = serialization['references']
10
+
11
+ worklist = Set.new(collect_references(roots))
12
+ visited = Set.new
13
+
14
+ while (live = worklist.first)
15
+ worklist.delete(live)
16
+ visited << live
17
+ collect_references(references[live]) do |ref|
18
+ worklist << ref unless visited.include?(ref)
19
+ end
20
+ end
21
+
22
+ references.keep_if { |ref, _val| visited.include?(ref) }
23
+ end
24
+
25
+ private
26
+
27
+ ## yield each reference encountered in tree
28
+ def collect_references(tree, &block)
29
+ return enum_for(__method__, tree) unless block_given?
30
+
31
+ case tree
32
+ when Hash
33
+ if tree.size == 1 && (ref = tree[ViewModel::REFERENCE_ATTRIBUTE])
34
+ block.(ref)
35
+ else
36
+ tree.each_value { |t| collect_references(t, &block) }
37
+ end
38
+ when Array
39
+ tree.each { |t| collect_references(t, &block) }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -52,6 +52,9 @@ module ViewModel::MigratableView
52
52
 
53
53
  graph = RGL::DirectedAdjacencyGraph.new
54
54
 
55
+ # Add a vertex for the current version, in case no edges reach it
56
+ graph.add_vertex(self.schema_version)
57
+
55
58
  # Add edges backwards, as we care about paths from the latest version
56
59
  @migration_classes.each_key do |from, to|
57
60
  graph.add_edge(to, from)
@@ -60,7 +63,7 @@ module ViewModel::MigratableView
60
63
  paths = graph.dijkstra_shortest_paths(Hash.new { 1 }, self.schema_version)
61
64
 
62
65
  paths.each do |target_version, path|
63
- next if path.length == 1
66
+ next if path.nil? || path.length == 1
64
67
 
65
68
  # Store the path forwards rather than backwards
66
69
  path_migration_classes = path.reverse.each_cons(2).map do |from, to|
@@ -4,6 +4,7 @@ class ViewModel::Migration::NoPathError < ViewModel::AbstractError
4
4
  attr_reader :vm_name, :from, :to
5
5
 
6
6
  status 400
7
+ code 'Migration.NoPathError'
7
8
 
8
9
  def initialize(viewmodel, from, to)
9
10
  @vm_name = viewmodel.view_name
@@ -4,6 +4,7 @@ class ViewModel::Migration::OneWayError < ViewModel::AbstractError
4
4
  attr_reader :vm_name, :direction
5
5
 
6
6
  status 400
7
+ code 'Migration.OneWayError'
7
8
 
8
9
  def initialize(vm_name, direction)
9
10
  @vm_name = vm_name
@@ -4,6 +4,7 @@ class ViewModel::Migration::UnspecifiedVersionError < ViewModel::AbstractError
4
4
  attr_reader :vm_name, :version
5
5
 
6
6
  status 400
7
+ code 'Migration.UnspecifiedVersionError'
7
8
 
8
9
  def initialize(vm_name, version)
9
10
  @vm_name = vm_name
@@ -2,6 +2,8 @@
2
2
 
3
3
  class ViewModel
4
4
  class Migrator
5
+ EXCLUDE_FROM_MIGRATION = '_exclude_from_migration'
6
+
5
7
  class << self
6
8
  def migrated_deep_schema_version(viewmodel_class, required_versions, include_referenced: true)
7
9
  deep_schema_version = viewmodel_class.deep_schema_version(include_referenced: include_referenced)
@@ -34,27 +36,37 @@ class ViewModel
34
36
  end
35
37
  end
36
38
 
37
- def migrate!(node, references:)
39
+ def migrate!(serialization)
40
+ migrate_tree!(serialization, references: serialization['references'] || {})
41
+ GarbageCollection.garbage_collect_references!(serialization)
42
+ end
43
+
44
+ private
45
+
46
+ def migrate_tree!(node, references:)
38
47
  case node
39
48
  when Hash
40
49
  if (type = node[ViewModel::TYPE_ATTRIBUTE])
41
50
  version = node[ViewModel::VERSION_ATTRIBUTE]
42
51
 
52
+ # We allow subtrees to be excluded from migration. This is used
53
+ # internally to permit stub references that are not a full
54
+ # serialization of the referenced type: see ViewModel::Cache.
55
+ return if node[EXCLUDE_FROM_MIGRATION]
56
+
43
57
  if migrate_viewmodel!(type, version, node, references)
44
58
  node[ViewModel::MIGRATED_ATTRIBUTE] = true
45
59
  end
46
60
  end
47
61
 
48
62
  node.each_value do |child|
49
- migrate!(child, references: references)
63
+ migrate_tree!(child, references: references)
50
64
  end
51
65
  when Array
52
- node.each { |child| migrate!(child, references: references) }
66
+ node.each { |child| migrate_tree!(child, references: references) }
53
67
  end
54
68
  end
55
69
 
56
- private
57
-
58
70
  def migrate_viewmodel!(_view_name, _version, _view_hash, _references)
59
71
  raise RuntimeError.new('abstract method')
60
72
  end
@@ -141,10 +141,10 @@ module ViewModelSpecHelpers
141
141
  end
142
142
  end
143
143
 
144
- module ParentAndBelongsToChildWithMigration
144
+ module ParentAndChildMigrations
145
145
  extend ActiveSupport::Concern
146
- include ViewModelSpecHelpers::ParentAndBelongsToChild
147
- def model_attributes
146
+
147
+ def model_attributes
148
148
  super.merge(
149
149
  schema: ->(t) { t.integer :new_field, default: 1, null: false },
150
150
  viewmodel: ->(_v) {
@@ -206,6 +206,12 @@ module ViewModelSpecHelpers
206
206
  end
207
207
  end
208
208
 
209
+ module ParentAndBelongsToChildWithMigration
210
+ extend ActiveSupport::Concern
211
+ include ViewModelSpecHelpers::ParentAndBelongsToChild
212
+ include ViewModelSpecHelpers::ParentAndChildMigrations
213
+ end
214
+
209
215
  module ParentAndSharedBelongsToChild
210
216
  extend ActiveSupport::Concern
211
217
  include ViewModelSpecHelpers::ParentAndBelongsToChild
@@ -122,7 +122,7 @@ class ViewModel::ActiveRecord
122
122
 
123
123
  if migration_versions.present?
124
124
  migrator = ViewModel::DownMigrator.new(migration_versions)
125
- migrator.migrate!([data, refs], references: refs)
125
+ migrator.migrate!({ 'data' => data, 'references' => refs })
126
126
  end
127
127
 
128
128
  [data, refs]
@@ -101,8 +101,10 @@ class ViewModel::ActiveRecord::HasManyThroughTest < ActiveSupport::TestCase
101
101
  @tag1, @tag2, @tag3 = (1..3).map { |x| Tag.create!(name: "tag#{x}") }
102
102
 
103
103
  @parent1 = Parent.create(name: 'p1',
104
- parents_tags: [ParentsTag.new(tag: @tag1, position: 1.0),
105
- ParentsTag.new(tag: @tag2, position: 2.0),])
104
+ parents_tags: [
105
+ ParentsTag.new(tag: @tag2, position: 2.0),
106
+ ParentsTag.new(tag: @tag1, position: 1.0),
107
+ ])
106
108
 
107
109
  enable_logging!
108
110
  end
@@ -12,147 +12,267 @@ class ViewModel::ActiveRecord::Migration < ActiveSupport::TestCase
12
12
  include ARVMTestUtilities
13
13
  extend Minitest::Spec::DSL
14
14
 
15
- include ViewModelSpecHelpers::ParentAndBelongsToChildWithMigration
16
-
17
15
  def new_model
18
16
  model_class.new(name: 'm1', child: child_model_class.new(name: 'c1'))
19
17
  end
20
18
 
21
19
  let(:viewmodel) { create_viewmodel! }
22
20
 
23
- let(:current_serialization) { ViewModel.serialize_to_hash(viewmodel) }
24
-
25
- let(:v2_serialization) do
26
- {
27
- ViewModel::TYPE_ATTRIBUTE => viewmodel_class.view_name,
28
- ViewModel::VERSION_ATTRIBUTE => 2,
29
- ViewModel::ID_ATTRIBUTE => viewmodel.id,
30
- 'name' => viewmodel.name,
31
- 'old_field' => 1,
32
- 'child' => {
33
- ViewModel::TYPE_ATTRIBUTE => child_viewmodel_class.view_name,
34
- ViewModel::VERSION_ATTRIBUTE => 2,
35
- ViewModel::ID_ATTRIBUTE => viewmodel.child.id,
36
- 'name' => viewmodel.child.name,
37
- 'former_field' => 'former_value',
38
- },
39
- }
40
- end
41
-
42
21
  let(:migration_versions) { { viewmodel_class => 2, child_viewmodel_class => 2 } }
43
22
 
44
23
  let(:down_migrator) { ViewModel::DownMigrator.new(migration_versions) }
45
24
  let(:up_migrator) { ViewModel::UpMigrator.new(migration_versions) }
46
25
 
47
26
  def migrate!
48
- migrator.migrate!(subject, references: {})
27
+ migrator.migrate!(subject)
49
28
  end
50
29
 
51
- describe 'downwards' do
52
- let(:migrator) { down_migrator }
53
- let(:subject) { current_serialization.deep_dup }
30
+ let(:current_serialization) do
31
+ ctx = viewmodel_class.new_serialize_context
32
+ view = ViewModel.serialize_to_hash(viewmodel, serialize_context: ctx)
33
+ refs = ctx.serialize_references_to_hash
34
+ { 'data' => view, 'references' => refs }
35
+ end
54
36
 
55
- let(:expected_result) do
56
- v2_serialization.deep_merge(
57
- {
58
- ViewModel::MIGRATED_ATTRIBUTE => true,
59
- 'old_field' => -1,
60
- 'child' => {
61
- ViewModel::MIGRATED_ATTRIBUTE => true,
62
- 'former_field' => 'reconstructed',
63
- },
37
+ let(:v2_serialization_data) do
38
+ {
39
+ ViewModel::TYPE_ATTRIBUTE => viewmodel_class.view_name,
40
+ ViewModel::VERSION_ATTRIBUTE => 2,
41
+ ViewModel::ID_ATTRIBUTE => viewmodel.id,
42
+ 'name' => viewmodel.name,
43
+ 'old_field' => 1,
44
+ 'child' => {
45
+ ViewModel::TYPE_ATTRIBUTE => child_viewmodel_class.view_name,
46
+ ViewModel::VERSION_ATTRIBUTE => 2,
47
+ ViewModel::ID_ATTRIBUTE => viewmodel.child.id,
48
+ 'name' => viewmodel.child.name,
49
+ 'former_field' => 'former_value',
64
50
  },
65
- )
66
- end
51
+ }
52
+ end
67
53
 
68
- it 'migrates' do
69
- migrate!
54
+ let(:v2_serialization_references) { {} }
70
55
 
71
- assert_equal(expected_result, subject)
72
- end
56
+ let(:v2_serialization) do
57
+ {
58
+ 'data' => v2_serialization_data,
59
+ 'references' => v2_serialization_references,
60
+ }
61
+ end
73
62
 
74
- describe 'to an unreachable version' do
75
- let(:migration_versions) { { viewmodel_class => 2, child_viewmodel_class => 1 } }
63
+ describe 'with defined migrations' do
64
+ include ViewModelSpecHelpers::ParentAndBelongsToChildWithMigration
76
65
 
77
- it 'raises' do
78
- assert_raises(ViewModel::Migration::NoPathError) do
79
- migrate!
80
- end
66
+ describe 'downwards' do
67
+ let(:migrator) { down_migrator }
68
+ let(:subject) { current_serialization.deep_dup }
69
+
70
+ let(:expected_result) do
71
+ v2_serialization.deep_merge(
72
+ {
73
+ 'data' => {
74
+ ViewModel::MIGRATED_ATTRIBUTE => true,
75
+ 'old_field' => -1,
76
+ 'child' => {
77
+ ViewModel::MIGRATED_ATTRIBUTE => true,
78
+ 'former_field' => 'reconstructed',
79
+ },
80
+ },
81
+ })
81
82
  end
82
- end
83
- end
84
83
 
85
- describe 'upwards' do
86
- let(:migrator) { up_migrator }
87
- let(:subject) { v2_serialization.deep_dup }
84
+ it 'migrates' do
85
+ migrate!
88
86
 
89
- let(:expected_result) do
90
- current_serialization.deep_merge(
91
- ViewModel::MIGRATED_ATTRIBUTE => true,
92
- 'new_field' => 3,
93
- 'child' => {
94
- ViewModel::MIGRATED_ATTRIBUTE => true,
95
- },
96
- )
97
- end
87
+ assert_equal(expected_result, subject)
88
+ end
98
89
 
99
- it 'migrates' do
100
- migrate!
90
+ describe 'to an unreachable version' do
91
+ let(:migration_versions) { { viewmodel_class => 2, child_viewmodel_class => 1 } }
101
92
 
102
- assert_equal(expected_result, subject)
93
+ it 'raises' do
94
+ assert_raises(ViewModel::Migration::NoPathError) do
95
+ migrate!
96
+ end
97
+ end
98
+ end
103
99
  end
104
100
 
105
- describe 'with version unspecified' do
106
- let(:subject) do
107
- v2_serialization
108
- .except(ViewModel::VERSION_ATTRIBUTE)
101
+ describe 'upwards' do
102
+ let(:migrator) { up_migrator }
103
+ let(:subject_data) { v2_serialization_data.deep_dup }
104
+ let(:subject_references) { v2_serialization_references.deep_dup }
105
+ let(:subject) { { 'data' => subject_data, 'references' => subject_references } }
106
+
107
+ let(:expected_result) do
108
+ current_serialization.deep_merge(
109
+ {
110
+ 'data' => {
111
+ ViewModel::MIGRATED_ATTRIBUTE => true,
112
+ 'new_field' => 3,
113
+ 'child' => {
114
+ ViewModel::MIGRATED_ATTRIBUTE => true,
115
+ },
116
+ },
117
+ }
118
+ )
109
119
  end
110
120
 
111
- it 'treats it as the requested version' do
121
+ it 'migrates' do
112
122
  migrate!
123
+
113
124
  assert_equal(expected_result, subject)
114
125
  end
115
- end
116
126
 
117
- describe 'with a version not in the specification' do
118
- let(:subject) do
119
- v2_serialization
120
- .except('old_field')
121
- .deep_merge(ViewModel::VERSION_ATTRIBUTE => 3, 'mid_field' => 1)
122
- end
127
+ describe 'with version unspecified' do
128
+ let(:subject_data) do
129
+ v2_serialization_data.except(ViewModel::VERSION_ATTRIBUTE)
130
+ end
123
131
 
124
- it 'rejects it' do
125
- assert_raises(ViewModel::Migration::UnspecifiedVersionError) do
132
+ it 'treats it as the requested version' do
126
133
  migrate!
134
+ assert_equal(expected_result, subject)
127
135
  end
128
136
  end
129
- end
130
137
 
131
- describe 'from an unreachable version' do
132
- let(:migration_versions) { { viewmodel_class => 2, child_viewmodel_class => 1 } }
138
+ describe 'with a version not in the specification' do
139
+ let(:subject_data) do
140
+ v2_serialization_data
141
+ .except('old_field')
142
+ .deep_merge(ViewModel::VERSION_ATTRIBUTE => 3, 'mid_field' => 1)
143
+ end
133
144
 
134
- let(:subject) do
135
- v2_serialization.deep_merge(
136
- 'child' => { ViewModel::VERSION_ATTRIBUTE => 1 },
137
- )
145
+ it 'rejects it' do
146
+ assert_raises(ViewModel::Migration::UnspecifiedVersionError) do
147
+ migrate!
148
+ end
149
+ end
138
150
  end
139
151
 
140
- it 'raises' do
141
- assert_raises(ViewModel::Migration::NoPathError) do
142
- migrate!
152
+ describe 'from an unreachable version' do
153
+ let(:migration_versions) { { viewmodel_class => 2, child_viewmodel_class => 1 } }
154
+
155
+ let(:subject_data) do
156
+ v2_serialization_data.deep_merge(
157
+ 'child' => { ViewModel::VERSION_ATTRIBUTE => 1 },
158
+ )
159
+ end
160
+
161
+ it 'raises' do
162
+ assert_raises(ViewModel::Migration::NoPathError) do
163
+ migrate!
164
+ end
165
+ end
166
+ end
167
+
168
+ describe 'in an undefined direction' do
169
+ let(:migration_versions) { { viewmodel_class => 1, child_viewmodel_class => 2 } }
170
+
171
+ let(:subject_data) do
172
+ v2_serialization_data.except('old_field').merge(ViewModel::VERSION_ATTRIBUTE => 1)
173
+ end
174
+
175
+ it 'raises' do
176
+ assert_raises(ViewModel::Migration::OneWayError) do
177
+ migrate!
178
+ end
143
179
  end
144
180
  end
145
181
  end
182
+ end
183
+
184
+ describe 'garbage collection' do
185
+ include ViewModelSpecHelpers::ParentAndSharedBelongsToChild
186
+
187
+ # current (v2) features the shared child, v1 did not
188
+ def model_attributes
189
+ super.merge(
190
+ viewmodel: ->(_v) {
191
+ self.schema_version = 2
192
+ migrates from: 1, to: 2 do
193
+ down do |view, refs|
194
+ view.delete('child')
195
+ end
196
+ end
197
+ })
198
+ end
146
199
 
147
- describe 'in an undefined direction' do
148
- let(:migration_versions) { { viewmodel_class => 1, child_viewmodel_class => 2 } }
200
+ # current (v2) refers to another child, v1 did not
201
+ def child_attributes
202
+ super.merge(
203
+ schema: ->(t) { t.references :child, foreign_key: true },
204
+ model: ->(m) {
205
+ belongs_to :child, inverse_of: :parent, dependent: :destroy
206
+ has_one :parent, inverse_of: :child, class_name: self.name
207
+ },
208
+ viewmodel: ->(_v) {
209
+ self.schema_version = 2
210
+ association :child
211
+ migrates from: 1, to: 2 do
212
+ down do |view, refs|
213
+ view.delete('child')
214
+ end
215
+ end
216
+ })
217
+ end
149
218
 
150
- let(:subject) do
151
- v2_serialization.except('old_field').merge(ViewModel::VERSION_ATTRIBUTE => 1)
219
+ def new_model
220
+ model_class.new(name: 'm1',
221
+ child: child_model_class.new(
222
+ name: 'c1',
223
+ child: child_model_class.new(
224
+ name: 'c2')))
225
+ end
226
+
227
+
228
+ let(:migrator) { down_migrator }
229
+ let(:migration_versions) { { viewmodel_class => 1, child_viewmodel_class => 1 } }
230
+
231
+ let(:subject) { current_serialization.deep_dup }
232
+
233
+ let(:expected_result) do
234
+ {
235
+ 'data' => {
236
+ ViewModel::TYPE_ATTRIBUTE => viewmodel_class.view_name,
237
+ ViewModel::VERSION_ATTRIBUTE => 1,
238
+ ViewModel::ID_ATTRIBUTE => viewmodel.id,
239
+ ViewModel::MIGRATED_ATTRIBUTE => true,
240
+ 'name' => viewmodel.name,
241
+ },
242
+ 'references' => {},
243
+ }
244
+ end
245
+
246
+ it 'migrates' do
247
+ migrate!
248
+
249
+ assert_equal(expected_result, subject)
250
+ end
251
+ end
252
+
253
+ describe 'without migrations' do
254
+ describe 'to an unreachable version' do
255
+ include ViewModelSpecHelpers::ParentAndBelongsToChild
256
+
257
+ def model_attributes
258
+ super.merge(viewmodel: ->(_v) {
259
+ self.schema_version = 4
260
+ # Define an unreachable migration to ensure that the view
261
+ # attempts to realize paths.
262
+ migrates from: 1, to: 2 do
263
+ end
264
+ })
265
+ end
266
+
267
+ def child_attributes
268
+ super.merge(viewmodel: ->(_v) { self.schema_version = 3 })
152
269
  end
153
270
 
154
- it 'raises' do
155
- assert_raises(ViewModel::Migration::OneWayError) do
271
+ let(:migrator) { down_migrator }
272
+ let(:subject) { current_serialization.deep_dup }
273
+
274
+ it 'raises no path error' do
275
+ assert_raises(ViewModel::Migration::NoPathError) do
156
276
  migrate!
157
277
  end
158
278
  end
@@ -416,6 +416,11 @@ class ViewModel::ActiveRecordTest < ActiveSupport::TestCase
416
416
  @list = List.new(child: List.new(child: nil))
417
417
  end
418
418
 
419
+ def test_list_eager_includes
420
+ expected_includes = 3.times.inject(nil) { |x| DeepPreloader::Spec.new({ 'child' => x }) }
421
+ assert_equal(expected_includes, ListView.eager_includes)
422
+ end
423
+
419
424
  def test_deserialize_context
420
425
  view = {
421
426
  '_type' => 'List',
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'minitest/unit'
5
+
6
+ require 'view_model'
7
+ require 'view_model/garbage_collection'
8
+
9
+ class ViewModel::GarbageCollectionTest < ActiveSupport::TestCase
10
+ extend Minitest::Spec::DSL
11
+
12
+ # Generate a viewmodel-serialization alike from a minimal structure
13
+ # @param [Hash<Symbol, Array<Symbol>] structure mapping from id to referenced ids
14
+ # @param [Hash<Symbol, Array<Symbol>] data_ids list of ids of data elements
15
+ def mock_serialization(data_skeleton, refs_skeleton)
16
+ data = []
17
+ references = {}
18
+
19
+ generate(data_skeleton) do |id, body|
20
+ data << body
21
+ end
22
+
23
+ generate(refs_skeleton) do |id, body|
24
+ references[id] = body
25
+ end
26
+
27
+ {
28
+ "data" => data,
29
+ "references" => references,
30
+ }
31
+ end
32
+
33
+ def generate(skeleton)
34
+ skeleton.each do |id, referred|
35
+ yield id, ({
36
+ ViewModel::ID_ATTRIBUTE => id,
37
+ :children => referred.map do |referred_id|
38
+ { ViewModel::REFERENCE_ATTRIBUTE => referred_id }
39
+ end
40
+ })
41
+ end
42
+ end
43
+
44
+ def retained_ids(data_skeleton, refs_skeleton)
45
+ serialization = mock_serialization(data_skeleton, refs_skeleton)
46
+ ViewModel::GarbageCollection.garbage_collect_references!(serialization)
47
+ Set.new(
48
+ (serialization['data'].map { |x| x[ViewModel::ID_ATTRIBUTE] }) +
49
+ (serialization['references'].keys),
50
+ )
51
+ end
52
+
53
+ it 'keeps all roots' do
54
+ assert_equal(
55
+ Set.new([:a, :b, :c]),
56
+ retained_ids({ a: [], b: [], c: [] }, {})
57
+ )
58
+ end
59
+
60
+ it 'keeps a list' do
61
+ assert_equal(
62
+ Set.new([:a, :b, :c, :d]),
63
+ retained_ids({ a: [:b], }, { b: [:c], c: [:d], d: [] }),
64
+ )
65
+ end
66
+
67
+ it 'keeps a child with a removed reference' do
68
+ assert_equal(
69
+ Set.new([:a, :z]),
70
+ retained_ids({ a: [:z], }, { b: [:z], z: [] }),
71
+ )
72
+ end
73
+
74
+ it 'prunes a list at the head' do
75
+ assert_equal(
76
+ Set.new([:a]),
77
+ retained_ids({ a: [], }, { b: [:c], c: [:d], d: [] }),
78
+ )
79
+ end
80
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iknow_view_models
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.8
4
+ version: 3.2.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - iKnow Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-03 00:00:00.000000000 Z
11
+ date: 2021-02-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -414,6 +414,7 @@ files:
414
414
  - lib/view_model/deserialize_context.rb
415
415
  - lib/view_model/error.rb
416
416
  - lib/view_model/error_view.rb
417
+ - lib/view_model/garbage_collection.rb
417
418
  - lib/view_model/migratable_view.rb
418
419
  - lib/view_model/migration.rb
419
420
  - lib/view_model/migration/no_path_error.rb
@@ -467,6 +468,7 @@ files:
467
468
  - test/unit/view_model/callbacks_test.rb
468
469
  - test/unit/view_model/controller_test.rb
469
470
  - test/unit/view_model/deserialization_error/unique_violation_test.rb
471
+ - test/unit/view_model/garbage_collection_test.rb
470
472
  - test/unit/view_model/record_test.rb
471
473
  - test/unit/view_model/registry_test.rb
472
474
  - test/unit/view_model/traversal_context_test.rb
@@ -527,6 +529,7 @@ test_files:
527
529
  - test/unit/view_model/callbacks_test.rb
528
530
  - test/unit/view_model/controller_test.rb
529
531
  - test/unit/view_model/deserialization_error/unique_violation_test.rb
532
+ - test/unit/view_model/garbage_collection_test.rb
530
533
  - test/unit/view_model/record_test.rb
531
534
  - test/unit/view_model/registry_test.rb
532
535
  - test/unit/view_model/traversal_context_test.rb