iknow_view_models 3.2.10 → 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: 280c8d764916ec30bb500e2f0e982131416d27ea51a3a25d1cd7e3908bb4ad5b
4
- data.tar.gz: 50d202b87cde141ac58ad82a5de6b73332f9b60a71c23fbbd378ba6c1def8184
3
+ metadata.gz: 2a75096a1b8ac47eee4ff5b2a303a0d447cfe8e93668f579bb697b863947eaa5
4
+ data.tar.gz: ca0edc9b15a0b436d36d7b12dbd8f4255008ad4a6cd6d1ed23a352a989013fa1
5
5
  SHA512:
6
- metadata.gz: 51333e3292118de4b71e8869ea4843be804ad2d24ce0c7296918c3f09b0df93740561afeb466b4a8874a1b5b667922b664745e994151b8fe47ce458bb087d14d
7
- data.tar.gz: 999d1feb717d4ef5ce11b400abf67772c5b70fd7c91cf3e328f14e5ff66a373b7c98aa5f701cb436cf4bf7df448bc553fca96131de570cead59734e8131c4788
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.10'
4
+ VERSION = '3.2.11'
5
5
  end
@@ -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'
@@ -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
@@ -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]
@@ -24,25 +24,39 @@ class ViewModel::ActiveRecord::Migration < ActiveSupport::TestCase
24
24
  let(:up_migrator) { ViewModel::UpMigrator.new(migration_versions) }
25
25
 
26
26
  def migrate!
27
- migrator.migrate!(subject, references: {})
27
+ migrator.migrate!(subject)
28
28
  end
29
29
 
30
- let(:current_serialization) { ViewModel.serialize_to_hash(viewmodel) }
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
31
36
 
32
- let(:v2_serialization) do
37
+ let(:v2_serialization_data) do
33
38
  {
34
- ViewModel::TYPE_ATTRIBUTE => viewmodel_class.view_name,
35
- ViewModel::VERSION_ATTRIBUTE => 2,
36
- ViewModel::ID_ATTRIBUTE => viewmodel.id,
37
- 'name' => viewmodel.name,
38
- 'old_field' => 1,
39
- 'child' => {
40
- ViewModel::TYPE_ATTRIBUTE => child_viewmodel_class.view_name,
39
+ ViewModel::TYPE_ATTRIBUTE => viewmodel_class.view_name,
41
40
  ViewModel::VERSION_ATTRIBUTE => 2,
42
- ViewModel::ID_ATTRIBUTE => viewmodel.child.id,
43
- 'name' => viewmodel.child.name,
44
- 'former_field' => 'former_value',
45
- },
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',
50
+ },
51
+ }
52
+ end
53
+
54
+ let(:v2_serialization_references) { {} }
55
+
56
+ let(:v2_serialization) do
57
+ {
58
+ 'data' => v2_serialization_data,
59
+ 'references' => v2_serialization_references,
46
60
  }
47
61
  end
48
62
 
@@ -56,14 +70,15 @@ class ViewModel::ActiveRecord::Migration < ActiveSupport::TestCase
56
70
  let(:expected_result) do
57
71
  v2_serialization.deep_merge(
58
72
  {
59
- ViewModel::MIGRATED_ATTRIBUTE => true,
60
- 'old_field' => -1,
61
- 'child' => {
73
+ 'data' => {
62
74
  ViewModel::MIGRATED_ATTRIBUTE => true,
63
- 'former_field' => 'reconstructed',
75
+ 'old_field' => -1,
76
+ 'child' => {
77
+ ViewModel::MIGRATED_ATTRIBUTE => true,
78
+ 'former_field' => 'reconstructed',
79
+ },
64
80
  },
65
- },
66
- )
81
+ })
67
82
  end
68
83
 
69
84
  it 'migrates' do
@@ -85,15 +100,21 @@ class ViewModel::ActiveRecord::Migration < ActiveSupport::TestCase
85
100
 
86
101
  describe 'upwards' do
87
102
  let(:migrator) { up_migrator }
88
- let(:subject) { v2_serialization.deep_dup }
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 } }
89
106
 
90
107
  let(:expected_result) do
91
108
  current_serialization.deep_merge(
92
- ViewModel::MIGRATED_ATTRIBUTE => true,
93
- 'new_field' => 3,
94
- 'child' => {
95
- ViewModel::MIGRATED_ATTRIBUTE => true,
96
- },
109
+ {
110
+ 'data' => {
111
+ ViewModel::MIGRATED_ATTRIBUTE => true,
112
+ 'new_field' => 3,
113
+ 'child' => {
114
+ ViewModel::MIGRATED_ATTRIBUTE => true,
115
+ },
116
+ },
117
+ }
97
118
  )
98
119
  end
99
120
 
@@ -104,9 +125,8 @@ class ViewModel::ActiveRecord::Migration < ActiveSupport::TestCase
104
125
  end
105
126
 
106
127
  describe 'with version unspecified' do
107
- let(:subject) do
108
- v2_serialization
109
- .except(ViewModel::VERSION_ATTRIBUTE)
128
+ let(:subject_data) do
129
+ v2_serialization_data.except(ViewModel::VERSION_ATTRIBUTE)
110
130
  end
111
131
 
112
132
  it 'treats it as the requested version' do
@@ -116,8 +136,8 @@ class ViewModel::ActiveRecord::Migration < ActiveSupport::TestCase
116
136
  end
117
137
 
118
138
  describe 'with a version not in the specification' do
119
- let(:subject) do
120
- v2_serialization
139
+ let(:subject_data) do
140
+ v2_serialization_data
121
141
  .except('old_field')
122
142
  .deep_merge(ViewModel::VERSION_ATTRIBUTE => 3, 'mid_field' => 1)
123
143
  end
@@ -132,8 +152,8 @@ class ViewModel::ActiveRecord::Migration < ActiveSupport::TestCase
132
152
  describe 'from an unreachable version' do
133
153
  let(:migration_versions) { { viewmodel_class => 2, child_viewmodel_class => 1 } }
134
154
 
135
- let(:subject) do
136
- v2_serialization.deep_merge(
155
+ let(:subject_data) do
156
+ v2_serialization_data.deep_merge(
137
157
  'child' => { ViewModel::VERSION_ATTRIBUTE => 1 },
138
158
  )
139
159
  end
@@ -148,8 +168,8 @@ class ViewModel::ActiveRecord::Migration < ActiveSupport::TestCase
148
168
  describe 'in an undefined direction' do
149
169
  let(:migration_versions) { { viewmodel_class => 1, child_viewmodel_class => 2 } }
150
170
 
151
- let(:subject) do
152
- v2_serialization.except('old_field').merge(ViewModel::VERSION_ATTRIBUTE => 1)
171
+ let(:subject_data) do
172
+ v2_serialization_data.except('old_field').merge(ViewModel::VERSION_ATTRIBUTE => 1)
153
173
  end
154
174
 
155
175
  it 'raises' do
@@ -161,6 +181,75 @@ class ViewModel::ActiveRecord::Migration < ActiveSupport::TestCase
161
181
  end
162
182
  end
163
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
199
+
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
218
+
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
+
164
253
  describe 'without migrations' do
165
254
  describe 'to an unreachable version' do
166
255
  include ViewModelSpecHelpers::ParentAndBelongsToChild
@@ -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.10
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: 2021-01-12 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