iknow_view_models 3.2.6 → 3.2.11

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89b29591362703c3bed79c996f0de85f7c8c1f65506bfd70b20f6abcf90f3375
4
- data.tar.gz: 81af7a5b3dd3d2c6531019fd65f10d5bcb382a943088eba6e5b16a0eb45d2253
3
+ metadata.gz: 2a75096a1b8ac47eee4ff5b2a303a0d447cfe8e93668f579bb697b863947eaa5
4
+ data.tar.gz: ca0edc9b15a0b436d36d7b12dbd8f4255008ad4a6cd6d1ed23a352a989013fa1
5
5
  SHA512:
6
- metadata.gz: 185fd079af273bbb14f617f38539b3b72f181e8bdc9af04151705267f6a4d3f21eed99d02401c4010f4d124f0d505b51f9283353bf7da0ae84fa62afbb093ad0
7
- data.tar.gz: 3b4dec46423b5bd7090de5c4f1e712e4ce5ef71e969838a52bed724bb42bef6b43c521a32e78c2b0ec948924a6f4311f05607472a4f6061e152413d465b867de
6
+ metadata.gz: 683a95fe51734a420f87f4709942314ec03ee32a8a95a63487bb4495eaad7a61c0f6ba4216d67ee31f33acf9f593607833eebcd3fd9e1dfd8a4f3440a337f249
7
+ data.tar.gz: e889492e1e0b76a3f8b3ed8775c16fa6f67f6a45eb9ed14e09f53f41b3dc6a1675d093eec75ba2b94d79567238d22d4996236a5388dafb5ee078fb9760d0c97e
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IknowViewModels
4
- VERSION = '3.2.6'
4
+ VERSION = '3.2.11'
5
5
  end
@@ -243,9 +243,17 @@ class ViewModel
243
243
  schema_version == self.schema_version
244
244
  end
245
245
 
246
+ def schema_versions(viewmodels)
247
+ viewmodels.each_with_object({}) do |view, h|
248
+ h[view.view_name] = view.schema_version
249
+ end
250
+ end
251
+
246
252
  def schema_hash(schema_versions)
247
253
  version_string = schema_versions.to_a.sort.join(',')
248
- Base64.urlsafe_encode64(Digest::MD5.digest(version_string), padding: false)
254
+ # We want a short hash value, as this will be used in cache keys
255
+ hash = Digest::SHA256.digest(version_string).byteslice(0, 16)
256
+ Base64.urlsafe_encode64(hash, padding: false)
249
257
  end
250
258
 
251
259
  def preload_for_serialization(viewmodels, serialize_context: new_serialize_context, include_referenced: true, lock: nil)
@@ -369,3 +377,4 @@ require 'view_model/deserialize_context'
369
377
  require 'view_model/changes'
370
378
  require 'view_model/schemas'
371
379
  require 'view_model/error_view'
380
+ require 'view_model/garbage_collection'
@@ -243,11 +243,10 @@ class ViewModel::ActiveRecord < ViewModel::Record
243
243
  end
244
244
 
245
245
  def deep_schema_version(include_referenced: true, include_external: true)
246
- (@deep_schema_version ||= {})[include_referenced] ||=
246
+ (@deep_schema_version ||= {})[[include_referenced, include_external]] ||=
247
247
  begin
248
- dependent_viewmodels(include_referenced: include_referenced, include_external: include_external).each_with_object({}) do |view, h|
249
- h[view.view_name] = view.schema_version
250
- end.freeze
248
+ vms = dependent_viewmodels(include_referenced: include_referenced, include_external: include_external)
249
+ ViewModel.schema_versions(vms).freeze
251
250
  end
252
251
  end
253
252
 
@@ -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]
@@ -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
@@ -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.6
4
+ version: 3.2.11
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-10-20 00:00:00.000000000 Z
11
+ date: 2021-01-27 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