iknow_view_models 3.2.7 → 3.2.12
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 +4 -4
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model.rb +7 -0
- data/lib/view_model/active_record.rb +9 -7
- data/lib/view_model/active_record/cache.rb +8 -9
- data/lib/view_model/active_record/controller.rb +2 -2
- data/lib/view_model/garbage_collection.rb +43 -0
- data/lib/view_model/migratable_view.rb +4 -1
- data/lib/view_model/migration/no_path_error.rb +1 -0
- data/lib/view_model/migration/one_way_error.rb +1 -0
- data/lib/view_model/migration/unspecified_version_error.rb +1 -0
- data/lib/view_model/migrator.rb +17 -5
- data/test/helpers/viewmodel_spec_helpers.rb +9 -3
- data/test/unit/view_model/active_record/cache_test.rb +1 -1
- data/test/unit/view_model/active_record/has_many_through_test.rb +4 -2
- data/test/unit/view_model/active_record/migration_test.rb +213 -93
- data/test/unit/view_model/garbage_collection_test.rb +80 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 284af292a0f1a70b761afd2e3ef084ad9f8f5853f00c8af8ac2d3b1227eb9cb5
|
|
4
|
+
data.tar.gz: 61fdea22a0b13db50e7b237e472900eef376e95d04fed5f21a8c1ef7422aaa56
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 64ffd808756c298236f4e00b11cdf7f1410019bc506f306c6cfbaa97b953212a5c58e7b56263deac0aac5e80398fa2d551922d6b90d7a3c60732a50b66467384
|
|
7
|
+
data.tar.gz: ccd3301209a94c8e9cd9a44166b153ae3edafab81cfe49498849e6c22dc413956d33e3cdba0ab001456122f47400df47dfbe327d9a9005f1947c512da3d9e523
|
data/lib/view_model.rb
CHANGED
|
@@ -243,6 +243,12 @@ 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
254
|
# We want a short hash value, as this will be used in cache keys
|
|
@@ -371,3 +377,4 @@ require 'view_model/deserialize_context'
|
|
|
371
377
|
require 'view_model/changes'
|
|
372
378
|
require 'view_model/schemas'
|
|
373
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)
|
|
249
|
-
|
|
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
|
|
|
@@ -341,12 +340,15 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
|
341
340
|
|
|
342
341
|
case
|
|
343
342
|
when association_data.through?
|
|
344
|
-
# associated here are
|
|
343
|
+
# associated here are join-table models; we need to get the far side out
|
|
344
|
+
join_models = associated
|
|
345
|
+
|
|
345
346
|
if association_data.direct_viewmodel._list_member?
|
|
346
|
-
|
|
347
|
+
attr = association_data.direct_viewmodel._list_attribute_name
|
|
348
|
+
join_models = join_models.sort_by { |j| j[attr] }
|
|
347
349
|
end
|
|
348
350
|
|
|
349
|
-
|
|
351
|
+
join_models.map do |through_model|
|
|
350
352
|
model = through_model.public_send(association_data.indirect_reflection.name)
|
|
351
353
|
association_data.viewmodel_class_for_model!(model.class).new(model)
|
|
352
354
|
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
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
#
|
|
189
|
-
#
|
|
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
|
|
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!(
|
|
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
|
|
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|
|
data/lib/view_model/migrator.rb
CHANGED
|
@@ -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!(
|
|
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
|
-
|
|
63
|
+
migrate_tree!(child, references: references)
|
|
50
64
|
end
|
|
51
65
|
when Array
|
|
52
|
-
node.each { |child|
|
|
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
|
|
144
|
+
module ParentAndChildMigrations
|
|
145
145
|
extend ActiveSupport::Concern
|
|
146
|
-
|
|
147
|
-
|
|
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!(
|
|
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: [
|
|
105
|
-
|
|
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
|
|
27
|
+
migrator.migrate!(subject)
|
|
49
28
|
end
|
|
50
29
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
51
|
+
}
|
|
52
|
+
end
|
|
67
53
|
|
|
68
|
-
|
|
69
|
-
migrate!
|
|
54
|
+
let(:v2_serialization_references) { {} }
|
|
70
55
|
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
let(:v2_serialization) do
|
|
57
|
+
{
|
|
58
|
+
'data' => v2_serialization_data,
|
|
59
|
+
'references' => v2_serialization_references,
|
|
60
|
+
}
|
|
61
|
+
end
|
|
73
62
|
|
|
74
|
-
|
|
75
|
-
|
|
63
|
+
describe 'with defined migrations' do
|
|
64
|
+
include ViewModelSpecHelpers::ParentAndBelongsToChildWithMigration
|
|
76
65
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
let(:subject) { v2_serialization.deep_dup }
|
|
84
|
+
it 'migrates' do
|
|
85
|
+
migrate!
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
100
|
-
|
|
90
|
+
describe 'to an unreachable version' do
|
|
91
|
+
let(:migration_versions) { { viewmodel_class => 2, child_viewmodel_class => 1 } }
|
|
101
92
|
|
|
102
|
-
|
|
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 '
|
|
106
|
-
let(:
|
|
107
|
-
|
|
108
|
-
|
|
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 '
|
|
121
|
+
it 'migrates' do
|
|
112
122
|
migrate!
|
|
123
|
+
|
|
113
124
|
assert_equal(expected_result, subject)
|
|
114
125
|
end
|
|
115
|
-
end
|
|
116
126
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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.
|
|
4
|
+
version: 3.2.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- iKnow Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2021-02-15 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
|