iknow_view_models 3.1.6 → 3.2.2

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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +6 -6
  3. data/.rubocop.yml +18 -0
  4. data/Appraisals +6 -6
  5. data/Gemfile +6 -2
  6. data/Rakefile +5 -5
  7. data/gemfiles/rails_5_2.gemfile +5 -5
  8. data/gemfiles/rails_6_0.gemfile +9 -0
  9. data/iknow_view_models.gemspec +40 -38
  10. data/lib/iknow_view_models.rb +9 -7
  11. data/lib/iknow_view_models/version.rb +1 -1
  12. data/lib/view_model.rb +31 -17
  13. data/lib/view_model/access_control.rb +5 -2
  14. data/lib/view_model/access_control/composed.rb +10 -9
  15. data/lib/view_model/access_control/open.rb +2 -0
  16. data/lib/view_model/access_control/read_only.rb +2 -0
  17. data/lib/view_model/access_control/tree.rb +11 -6
  18. data/lib/view_model/access_control_error.rb +4 -1
  19. data/lib/view_model/active_record.rb +13 -12
  20. data/lib/view_model/active_record/association_data.rb +3 -2
  21. data/lib/view_model/active_record/association_manipulation.rb +6 -4
  22. data/lib/view_model/active_record/cache.rb +114 -34
  23. data/lib/view_model/active_record/cache/cacheable_view.rb +2 -2
  24. data/lib/view_model/active_record/collection_nested_controller.rb +3 -3
  25. data/lib/view_model/active_record/controller.rb +68 -1
  26. data/lib/view_model/active_record/controller_base.rb +4 -1
  27. data/lib/view_model/active_record/nested_controller_base.rb +1 -0
  28. data/lib/view_model/active_record/update_context.rb +8 -6
  29. data/lib/view_model/active_record/update_data.rb +32 -30
  30. data/lib/view_model/active_record/update_operation.rb +17 -13
  31. data/lib/view_model/active_record/visitor.rb +0 -1
  32. data/lib/view_model/after_transaction_runner.rb +2 -2
  33. data/lib/view_model/callbacks.rb +3 -1
  34. data/lib/view_model/controller.rb +13 -3
  35. data/lib/view_model/deserialization_error.rb +15 -12
  36. data/lib/view_model/error.rb +12 -10
  37. data/lib/view_model/error_view.rb +3 -1
  38. data/lib/view_model/migratable_view.rb +78 -0
  39. data/lib/view_model/migration.rb +48 -0
  40. data/lib/view_model/migration/no_path_error.rb +26 -0
  41. data/lib/view_model/migration/one_way_error.rb +24 -0
  42. data/lib/view_model/migration/unspecified_version_error.rb +24 -0
  43. data/lib/view_model/migrator.rb +108 -0
  44. data/lib/view_model/record.rb +15 -14
  45. data/lib/view_model/reference.rb +3 -1
  46. data/lib/view_model/references.rb +8 -5
  47. data/lib/view_model/registry.rb +1 -1
  48. data/lib/view_model/schemas.rb +9 -4
  49. data/lib/view_model/serialization_error.rb +4 -1
  50. data/lib/view_model/serialize_context.rb +4 -4
  51. data/lib/view_model/test_helpers.rb +8 -3
  52. data/lib/view_model/test_helpers/arvm_builder.rb +21 -15
  53. data/lib/view_model/traversal_context.rb +2 -1
  54. data/nix/dependencies.nix +5 -0
  55. data/nix/gem/generate.rb +2 -1
  56. data/shell.nix +8 -3
  57. data/test/.rubocop.yml +14 -0
  58. data/test/helpers/arvm_test_models.rb +12 -9
  59. data/test/helpers/arvm_test_utilities.rb +5 -3
  60. data/test/helpers/controller_test_helpers.rb +55 -32
  61. data/test/helpers/match_enumerator.rb +1 -0
  62. data/test/helpers/query_logging.rb +2 -1
  63. data/test/helpers/test_access_control.rb +5 -3
  64. data/test/helpers/viewmodel_spec_helpers.rb +88 -22
  65. data/test/unit/view_model/access_control_test.rb +144 -144
  66. data/test/unit/view_model/active_record/alias_test.rb +15 -13
  67. data/test/unit/view_model/active_record/belongs_to_test.rb +40 -39
  68. data/test/unit/view_model/active_record/cache_test.rb +68 -31
  69. data/test/unit/view_model/active_record/cloner_test.rb +67 -63
  70. data/test/unit/view_model/active_record/controller_test.rb +113 -65
  71. data/test/unit/view_model/active_record/counter_test.rb +10 -9
  72. data/test/unit/view_model/active_record/customization_test.rb +59 -58
  73. data/test/unit/view_model/active_record/has_many_test.rb +112 -111
  74. data/test/unit/view_model/active_record/has_many_through_poly_test.rb +15 -14
  75. data/test/unit/view_model/active_record/has_many_through_test.rb +33 -38
  76. data/test/unit/view_model/active_record/has_one_test.rb +37 -36
  77. data/test/unit/view_model/active_record/migration_test.rb +161 -0
  78. data/test/unit/view_model/active_record/namespacing_test.rb +19 -17
  79. data/test/unit/view_model/active_record/poly_test.rb +44 -45
  80. data/test/unit/view_model/active_record/shared_test.rb +30 -28
  81. data/test/unit/view_model/active_record/version_test.rb +9 -7
  82. data/test/unit/view_model/active_record_test.rb +72 -72
  83. data/test/unit/view_model/callbacks_test.rb +19 -15
  84. data/test/unit/view_model/controller_test.rb +4 -2
  85. data/test/unit/view_model/record_test.rb +92 -97
  86. data/test/unit/view_model/traversal_context_test.rb +4 -5
  87. data/test/unit/view_model_test.rb +18 -16
  88. metadata +36 -12
  89. data/.travis.yml +0 -31
  90. data/appveyor.yml +0 -22
  91. data/gemfiles/rails_6_0_beta.gemfile +0 -9
@@ -91,5 +91,4 @@ class ViewModel::ActiveRecord::Visitor
91
91
  def reset_state!
92
92
  @ignored_associations = Set.new
93
93
  end
94
-
95
94
  end
@@ -33,11 +33,11 @@ module ViewModel::AfterTransactionRunner
33
33
  if connection.transaction_open?
34
34
  connection.add_transaction_record(self)
35
35
  else
36
- after_transaction
36
+ before_commit
37
+ after_commit
37
38
  end
38
39
  end
39
40
 
40
-
41
41
  # Override to tie to a specific connection.
42
42
  def connection
43
43
  ActiveRecord::Base.connection
@@ -79,7 +79,7 @@ module ViewModel::Callbacks
79
79
  end
80
80
  SRC
81
81
  else
82
- def self.create(callbacks, view, context)
82
+ def self.create(callbacks, view, context) # rubocop:disable Lint/NestedMethodDefinition
83
83
  self.new(callbacks, view, context)
84
84
  end
85
85
  end
@@ -105,6 +105,7 @@ module ViewModel::Callbacks
105
105
  define_singleton_method(:class_callbacks) { base_callbacks }
106
106
  define_singleton_method(:all_callbacks) do |&block|
107
107
  return to_enum(__method__) unless block
108
+
108
109
  block.call(base_callbacks)
109
110
  end
110
111
  end
@@ -115,6 +116,7 @@ module ViewModel::Callbacks
115
116
  subclass.define_singleton_method(:class_callbacks) { subclass_callbacks }
116
117
  subclass.define_singleton_method(:all_callbacks) do |&block|
117
118
  return to_enum(__method__) unless block
119
+
118
120
  super(&block)
119
121
  block.call(subclass_callbacks)
120
122
  end
@@ -88,6 +88,7 @@ module ViewModel::Controller
88
88
  if data.blank?
89
89
  raise ViewModel::Error.new(status: 400, detail: "No data submitted: #{data.inspect}")
90
90
  end
91
+
91
92
  data.map { |el| _extract_param_hash(el) }
92
93
  else
93
94
  _extract_param_hash(data)
@@ -129,9 +130,18 @@ module ViewModel::Controller
129
130
  # untouched. Requires a MultiJson adapter other than ActiveSupport's
130
131
  # (modified) JsonGem.
131
132
  class CompiledJson
132
- def initialize(s); @s = s; end
133
- def to_json(*args); @s; end
134
- def to_s; @s; end
133
+ def initialize(s)
134
+ @s = s
135
+ end
136
+
137
+ def to_json(*_args)
138
+ @s
139
+ end
140
+
141
+ def to_s
142
+ @s
143
+ end
144
+
135
145
  undef_method :as_json
136
146
  end
137
147
 
@@ -15,13 +15,14 @@ class ViewModel
15
15
  unless nodes.all? { |n| n.viewmodel_class == first }
16
16
  raise ArgumentError.new("All nodes must be of the same type for #{self.class.name}")
17
17
  end
18
+
18
19
  first
19
20
  end
20
21
 
21
22
  # A collection of DeserializationErrors
22
23
  class Collection < ViewModel::AbstractErrorCollection
23
- title "Error(s) occurred during deserialization"
24
- code "DeserializationError.Collection"
24
+ title 'Error(s) occurred during deserialization'
25
+ code 'DeserializationError.Collection'
25
26
 
26
27
  def detail
27
28
  "Error(s) occurred during deserialization: #{cause_details}"
@@ -33,7 +34,7 @@ class ViewModel
33
34
  class InvalidRequest < DeserializationError
34
35
  # Abstract
35
36
  status 400
36
- title "Invalid request"
37
+ title 'Invalid request'
37
38
  end
38
39
 
39
40
  # There has been an unexpected internal failure of the ViewModel library.
@@ -145,6 +146,7 @@ class ViewModel
145
146
  # association.
146
147
  class InvalidAssociationType < InvalidRequest
147
148
  attr_reader :association, :target_type
149
+
148
150
  def initialize(association, target_type, node)
149
151
  @association = association
150
152
  @target_type = target_type
@@ -198,7 +200,7 @@ class ViewModel
198
200
  end
199
201
 
200
202
  def detail
201
- errors = missing_nodes.map(&:to_s).join(", ")
203
+ errors = missing_nodes.map(&:to_s).join(', ')
202
204
  "Couldn't find requested member node(s) in association '#{association}': "\
203
205
  "#{errors}"
204
206
  end
@@ -218,7 +220,7 @@ class ViewModel
218
220
  end
219
221
 
220
222
  def detail
221
- "Duplicate views for the same '#{type}' specified: "+ nodes.map(&:to_s).join(", ")
223
+ "Duplicate views for the same '#{type}' specified: " + nodes.map(&:to_s).join(', ')
222
224
  end
223
225
 
224
226
  def meta
@@ -235,14 +237,14 @@ class ViewModel
235
237
  end
236
238
 
237
239
  def detail
238
- "Multiple parents attempted to claim the same owned '#{association_name}' reference: " + nodes.map(&:to_s).join(", ")
240
+ "Multiple parents attempted to claim the same owned '#{association_name}' reference: " + nodes.map(&:to_s).join(', ')
239
241
  end
240
242
  end
241
243
 
242
244
  class ParentNotFound < NotFound
243
245
  def detail
244
- "Could not resolve release from previous parent for the following owned viewmodel(s): " +
245
- nodes.map(&:to_s).join(", ")
246
+ 'Could not resolve release from previous parent for the following owned viewmodel(s): ' +
247
+ nodes.map(&:to_s).join(', ')
246
248
  end
247
249
  end
248
250
 
@@ -284,7 +286,7 @@ class ViewModel
284
286
 
285
287
  class ReadOnlyType < DeserializationError
286
288
  status 400
287
- detail "Deserialization not defined for view type"
289
+ detail 'Deserialization not defined for view type'
288
290
  end
289
291
 
290
292
  class InvalidAttributeType < InvalidRequest
@@ -326,7 +328,7 @@ class ViewModel
326
328
  status 400
327
329
 
328
330
  def detail
329
- errors = nodes.map(&:to_s).join(", ")
331
+ errors = nodes.map(&:to_s).join(', ')
330
332
  "Optimistic lock failure updating nodes: #{errors}"
331
333
  end
332
334
  end
@@ -408,8 +410,9 @@ class ViewModel
408
410
 
409
411
  private
410
412
 
411
- QUOTED_IDENTIFIER = /\A"(?:[^"]|"")+"/
412
- UNQUOTED_IDENTIFIER = /\A(?:\p{Alpha}|_)(?:\p{Alnum}|_)*/
413
+ QUOTED_IDENTIFIER = /\A"(?:[^"]|"")+"/.freeze
414
+ UNQUOTED_IDENTIFIER = /\A(?:\p{Alpha}|_)(?:\p{Alnum}|_)*/.freeze
415
+
413
416
  def parse_identifier(stream)
414
417
  if (identifier = stream.slice!(UNQUOTED_IDENTIFIER))
415
418
  identifier
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Abstract base for renderable errors in ViewModel-based APIs. Errors of this
2
4
  # type will be caught by ViewModel controllers and rendered in a standard format
3
5
  # by ViewModel::ErrorView, which loosely follows errors in JSON-API.
@@ -6,7 +8,7 @@ class ViewModel::AbstractError < StandardError
6
8
  # Brief DSL for quickly defining constant attribute values in subclasses
7
9
  [:detail, :status, :title, :code].each do |attribute|
8
10
  define_method(attribute) do |x|
9
- define_method(attribute){ x }
11
+ define_method(attribute) { x }
10
12
  end
11
13
  end
12
14
  end
@@ -25,7 +27,7 @@ class ViewModel::AbstractError < StandardError
25
27
 
26
28
  # Human-readable reason for use displaying this error.
27
29
  def detail
28
- "ViewModel::AbstractError"
30
+ 'ViewModel::AbstractError'
29
31
  end
30
32
 
31
33
  # HTTP status code most appropriate for this error
@@ -40,7 +42,7 @@ class ViewModel::AbstractError < StandardError
40
42
 
41
43
  # Unique symbol identifying this error type
42
44
  def code
43
- "ViewModel.AbstractError"
45
+ 'ViewModel.AbstractError'
44
46
  end
45
47
 
46
48
  # Additional information specific to this error type.
@@ -74,8 +76,6 @@ class ViewModel::AbstractError < StandardError
74
76
 
75
77
  protected
76
78
 
77
-
78
-
79
79
  def format_references(viewmodel_refs)
80
80
  viewmodel_refs.map do |viewmodel_ref|
81
81
  format_reference(viewmodel_ref)
@@ -85,7 +85,7 @@ class ViewModel::AbstractError < StandardError
85
85
  def format_reference(viewmodel_ref)
86
86
  {
87
87
  ViewModel::TYPE_ATTRIBUTE => viewmodel_ref.viewmodel_class.view_name,
88
- ViewModel::ID_ATTRIBUTE => viewmodel_ref.model_id
88
+ ViewModel::ID_ATTRIBUTE => viewmodel_ref.model_id,
89
89
  }
90
90
  end
91
91
  end
@@ -100,12 +100,13 @@ class ViewModel::AbstractErrorWithBlame < ViewModel::AbstractError
100
100
  unless @nodes.all? { |n| n.is_a?(ViewModel::Reference) }
101
101
  raise ArgumentError.new("#{self.class.name}: 'blame_nodes' must all be of type ViewModel::Reference")
102
102
  end
103
+
103
104
  super()
104
105
  end
105
106
 
106
107
  def meta
107
108
  {
108
- nodes: format_references(nodes)
109
+ nodes: format_references(nodes),
109
110
  }
110
111
  end
111
112
  end
@@ -117,8 +118,9 @@ class ViewModel::AbstractErrorCollection < ViewModel::AbstractError
117
118
  def initialize(causes)
118
119
  @causes = Array.wrap(causes)
119
120
  unless @causes.present?
120
- raise ArgumentError.new("A collection must have at least one cause")
121
+ raise ArgumentError.new('A collection must have at least one cause')
121
122
  end
123
+
122
124
  super()
123
125
  end
124
126
 
@@ -151,7 +153,7 @@ class ViewModel::AbstractErrorCollection < ViewModel::AbstractError
151
153
  protected
152
154
 
153
155
  def cause_details
154
- causes.map(&:detail).join("; ")
156
+ causes.map(&:detail).join('; ')
155
157
  end
156
158
  end
157
159
 
@@ -180,7 +182,7 @@ end
180
182
  class ViewModel::Error < ViewModel::AbstractError
181
183
  attr_reader :detail, :status, :title, :code, :meta
182
184
 
183
- def initialize(status: 400, detail: "ViewModel Error", title: nil, code: nil, meta: {})
185
+ def initialize(status: 400, detail: 'ViewModel Error', title: nil, code: nil, meta: {})
184
186
  @detail = detail
185
187
  @status = status
186
188
  @title = title
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'view_model/record'
2
4
 
3
5
  # ViewModel for rendering ViewModel::AbstractErrors
@@ -10,7 +12,7 @@ class ViewModel::ErrorView < ViewModel::Record
10
12
  def serialize_view(json, serialize_context: nil)
11
13
  json.set! :class, exception.class.name
12
14
  json.backtrace exception.backtrace
13
- if cause = exception.cause
15
+ if (cause = exception.cause)
14
16
  json.cause do
15
17
  json.set! :class, cause.class.name
16
18
  json.backtrace cause.backtrace
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'view_model/migration'
4
+ require 'view_model/migrator'
5
+
6
+ require 'rgl/adjacency'
7
+ require 'rgl/dijkstra'
8
+
9
+ module ViewModel::MigratableView
10
+ extend ActiveSupport::Concern
11
+
12
+ class_methods do
13
+ def inherited(base)
14
+ super
15
+ base.initialize_as_migratable_view
16
+ end
17
+
18
+ def initialize_as_migratable_view
19
+ @migrations_lock = Monitor.new
20
+ @migration_classes = {}
21
+ @migration_paths = {}
22
+ @realized_migration_paths = true
23
+ end
24
+
25
+ def migration_path(from:, to:)
26
+ @migrations_lock.synchronize do
27
+ realize_paths! unless @realized_migration_paths
28
+
29
+ migrations = @migration_paths.fetch([from, to]) do
30
+ raise ViewModel::Migration::NoPathError.new(self, from, to)
31
+ end
32
+
33
+ migrations
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ # Define a migration on this viewmodel
40
+ def migrates(from:, to:, &block)
41
+ @migrations_lock.synchronize do
42
+ builder = ViewModel::Migration::Builder.new
43
+ builder.instance_exec(&block)
44
+ @migration_classes[[from, to]] = builder.build!
45
+ @realized_migration_paths = false
46
+ end
47
+ end
48
+
49
+ # Internal: find and record possible paths to the current schema version.
50
+ def realize_paths!
51
+ @migration_paths.clear
52
+
53
+ graph = RGL::DirectedAdjacencyGraph.new
54
+
55
+ # Add edges backwards, as we care about paths from the latest version
56
+ @migration_classes.each_key do |from, to|
57
+ graph.add_edge(to, from)
58
+ end
59
+
60
+ paths = graph.dijkstra_shortest_paths(Hash.new { 1 }, self.schema_version)
61
+
62
+ paths.each do |target_version, path|
63
+ next if path.length == 1
64
+
65
+ # Store the path forwards rather than backwards
66
+ path_migration_classes = path.reverse.each_cons(2).map do |from, to|
67
+ @migration_classes.fetch([from, to])
68
+ end
69
+
70
+ key = [target_version, schema_version]
71
+
72
+ @migration_paths[key] = path_migration_classes.map(&:new)
73
+ end
74
+
75
+ @realized_paths = true
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ViewModel::Migration
4
+ require 'view_model/migration/no_path_error'
5
+ require 'view_model/migration/one_way_error'
6
+ require 'view_model/migration/unspecified_version_error'
7
+
8
+ def up(view, _references)
9
+ raise ViewModel::Migration::OneWayError.new(view[ViewModel::TYPE_ATTRIBUTE], :up)
10
+ end
11
+
12
+ def down(view, _references)
13
+ raise ViewModel::Migration::OneWayError.new(view[ViewModel::TYPE_ATTRIBUTE], :down)
14
+ end
15
+
16
+ # Tiny DSL for defining migration classes
17
+ class Builder
18
+ def initialize
19
+ @up_block = nil
20
+ @down_block = nil
21
+ end
22
+
23
+ def build!
24
+ migration = Class.new(ViewModel::Migration)
25
+ migration.define_method(:up, &@up_block) if @up_block
26
+ migration.define_method(:down, &@down_block) if @down_block
27
+ migration
28
+ end
29
+
30
+ private
31
+
32
+ def up(&block)
33
+ check_signature!(block)
34
+ @up_block = block
35
+ end
36
+
37
+ def down(&block)
38
+ check_signature!(block)
39
+ @down_block = block
40
+ end
41
+
42
+ def check_signature!(block)
43
+ unless block.arity == 2
44
+ raise RuntimeError.new('Illegal signature for migration method, must be (view, references)')
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ViewModel::Migration::NoPathError < ViewModel::AbstractError
4
+ attr_reader :vm_name, :from, :to
5
+
6
+ status 400
7
+
8
+ def initialize(viewmodel, from, to)
9
+ @vm_name = viewmodel.view_name
10
+ @from = from
11
+ @to = to
12
+ super()
13
+ end
14
+
15
+ def detail
16
+ "No migration path for #{vm_name} from #{from} to #{to}"
17
+ end
18
+
19
+ def meta
20
+ {
21
+ viewmodel: vm_name,
22
+ from: from,
23
+ to: to,
24
+ }
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ViewModel::Migration::OneWayError < ViewModel::AbstractError
4
+ attr_reader :vm_name, :direction
5
+
6
+ status 400
7
+
8
+ def initialize(vm_name, direction)
9
+ @vm_name = vm_name
10
+ @direction = direction
11
+ super()
12
+ end
13
+
14
+ def detail
15
+ "One way migration for #{vm_name} cannot be migrated #{direction}"
16
+ end
17
+
18
+ def meta
19
+ {
20
+ viewmodel: vm_name,
21
+ direction: direction,
22
+ }
23
+ end
24
+ end