iknow_view_models 3.1.5 → 3.2.1

Sign up to get free protection for your applications and to get access to all the features.
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 +53 -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 +8 -5
  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 +44 -28
  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 +71 -38
  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