iknow_view_models 3.2.10 → 3.2.11

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