iknow_view_models 3.2.0 → 3.2.1

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +13 -0
  3. data/Appraisals +6 -6
  4. data/Rakefile +5 -5
  5. data/gemfiles/rails_5_2.gemfile +5 -5
  6. data/gemfiles/rails_6_0.gemfile +5 -5
  7. data/iknow_view_models.gemspec +40 -39
  8. data/lib/iknow_view_models.rb +9 -7
  9. data/lib/iknow_view_models/version.rb +1 -1
  10. data/lib/view_model.rb +17 -14
  11. data/lib/view_model/access_control.rb +5 -2
  12. data/lib/view_model/access_control/composed.rb +10 -9
  13. data/lib/view_model/access_control/open.rb +2 -0
  14. data/lib/view_model/access_control/read_only.rb +2 -0
  15. data/lib/view_model/access_control/tree.rb +11 -6
  16. data/lib/view_model/access_control_error.rb +4 -1
  17. data/lib/view_model/active_record.rb +12 -11
  18. data/lib/view_model/active_record/association_data.rb +2 -1
  19. data/lib/view_model/active_record/association_manipulation.rb +6 -4
  20. data/lib/view_model/active_record/cache.rb +4 -2
  21. data/lib/view_model/active_record/collection_nested_controller.rb +3 -3
  22. data/lib/view_model/active_record/controller_base.rb +4 -1
  23. data/lib/view_model/active_record/nested_controller_base.rb +1 -0
  24. data/lib/view_model/active_record/update_context.rb +8 -6
  25. data/lib/view_model/active_record/update_data.rb +32 -30
  26. data/lib/view_model/active_record/update_operation.rb +17 -13
  27. data/lib/view_model/active_record/visitor.rb +0 -1
  28. data/lib/view_model/after_transaction_runner.rb +0 -1
  29. data/lib/view_model/callbacks.rb +3 -1
  30. data/lib/view_model/controller.rb +13 -3
  31. data/lib/view_model/deserialization_error.rb +15 -12
  32. data/lib/view_model/error.rb +12 -10
  33. data/lib/view_model/error_view.rb +3 -1
  34. data/lib/view_model/migration/no_path_error.rb +1 -0
  35. data/lib/view_model/migration/one_way_error.rb +1 -0
  36. data/lib/view_model/migration/unspecified_version_error.rb +1 -0
  37. data/lib/view_model/record.rb +11 -13
  38. data/lib/view_model/reference.rb +3 -1
  39. data/lib/view_model/references.rb +8 -5
  40. data/lib/view_model/registry.rb +1 -1
  41. data/lib/view_model/schemas.rb +9 -4
  42. data/lib/view_model/serialization_error.rb +4 -1
  43. data/lib/view_model/serialize_context.rb +4 -4
  44. data/lib/view_model/test_helpers.rb +8 -3
  45. data/lib/view_model/test_helpers/arvm_builder.rb +19 -14
  46. data/lib/view_model/traversal_context.rb +2 -1
  47. data/test/.rubocop.yml +14 -0
  48. data/test/helpers/arvm_test_models.rb +12 -9
  49. data/test/helpers/arvm_test_utilities.rb +5 -3
  50. data/test/helpers/controller_test_helpers.rb +31 -29
  51. data/test/helpers/match_enumerator.rb +1 -0
  52. data/test/helpers/query_logging.rb +2 -1
  53. data/test/helpers/test_access_control.rb +5 -3
  54. data/test/helpers/viewmodel_spec_helpers.rb +21 -20
  55. data/test/unit/view_model/access_control_test.rb +144 -144
  56. data/test/unit/view_model/active_record/alias_test.rb +15 -13
  57. data/test/unit/view_model/active_record/belongs_to_test.rb +40 -39
  58. data/test/unit/view_model/active_record/cache_test.rb +27 -26
  59. data/test/unit/view_model/active_record/cloner_test.rb +67 -63
  60. data/test/unit/view_model/active_record/controller_test.rb +37 -38
  61. data/test/unit/view_model/active_record/counter_test.rb +10 -9
  62. data/test/unit/view_model/active_record/customization_test.rb +59 -58
  63. data/test/unit/view_model/active_record/has_many_test.rb +112 -111
  64. data/test/unit/view_model/active_record/has_many_through_poly_test.rb +15 -14
  65. data/test/unit/view_model/active_record/has_many_through_test.rb +33 -38
  66. data/test/unit/view_model/active_record/has_one_test.rb +37 -36
  67. data/test/unit/view_model/active_record/migration_test.rb +13 -13
  68. data/test/unit/view_model/active_record/namespacing_test.rb +19 -17
  69. data/test/unit/view_model/active_record/poly_test.rb +44 -45
  70. data/test/unit/view_model/active_record/shared_test.rb +30 -28
  71. data/test/unit/view_model/active_record/version_test.rb +9 -7
  72. data/test/unit/view_model/active_record_test.rb +72 -72
  73. data/test/unit/view_model/callbacks_test.rb +19 -15
  74. data/test/unit/view_model/controller_test.rb +4 -2
  75. data/test/unit/view_model/record_test.rb +92 -97
  76. data/test/unit/view_model/traversal_context_test.rb +4 -5
  77. data/test/unit/view_model_test.rb +18 -16
  78. metadata +7 -5
@@ -1,4 +1,6 @@
1
- require "renum"
1
+ # frozen_string_literal: true
2
+
3
+ require 'renum'
2
4
 
3
5
  # Partially parsed tree of user-specified update hashes, created during deserialization.
4
6
  class ViewModel::ActiveRecord
@@ -14,7 +16,7 @@ class ViewModel::ActiveRecord
14
16
  :update_data,
15
17
  :points_to, # AssociationData => UpdateOperation (returns single new viewmodel to update fkey)
16
18
  :pointed_to, # AssociationData => UpdateOperation(s) (returns viewmodel(s) with which to update assoc cache)
17
- :reparent_to, # If node needs to update its pointer to a new parent, ParentData for the parent
19
+ :reparent_to, # If node needs to update its pointer to a new parent, ParentData for the parent
18
20
  :reposition_to, # if this node participates in a list under its parent, what should its position be?
19
21
  :released_children # Set of children that have been released
20
22
 
@@ -46,11 +48,11 @@ class ViewModel::ActiveRecord
46
48
 
47
49
  # Evaluate a built update tree, applying and saving changes to the models.
48
50
  def run!(deserialize_context:)
49
- raise ViewModel::DeserializationError::Internal.new("Internal error: UpdateOperation run before build") unless built?
51
+ raise ViewModel::DeserializationError::Internal.new('Internal error: UpdateOperation run before build') unless built?
50
52
 
51
53
  case @run_state
52
54
  when RunState::Running
53
- raise ViewModel::DeserializationError::Internal.new("Internal error: Cycle found in running UpdateOperation")
55
+ raise ViewModel::DeserializationError::Internal.new('Internal error: Cycle found in running UpdateOperation')
54
56
  when RunState::Run
55
57
  return viewmodel
56
58
  end
@@ -217,7 +219,7 @@ class ViewModel::ActiveRecord
217
219
 
218
220
  # Recursively builds UpdateOperations for the associations in our UpdateData
219
221
  def build!(update_context)
220
- raise ViewModel::DeserializationError::Internal.new("Internal error: UpdateOperation cannot build a deferred update") if viewmodel.nil?
222
+ raise ViewModel::DeserializationError::Internal.new('Internal error: UpdateOperation cannot build a deferred update') if viewmodel.nil?
221
223
  return self if built?
222
224
 
223
225
  update_data.associations.each do |association_name, association_update_data|
@@ -254,8 +256,8 @@ class ViewModel::ActiveRecord
254
256
  def add_update(association_data, update)
255
257
  target =
256
258
  case association_data.pointer_location
257
- when :remote; pointed_to
258
- when :local; points_to
259
+ when :remote then pointed_to
260
+ when :local then points_to
259
261
  end
260
262
 
261
263
  target[association_data] = update
@@ -658,7 +660,7 @@ class ViewModel::ActiveRecord
658
660
  other.indirect_viewmodel_reference == self.indirect_viewmodel_reference
659
661
  end
660
662
 
661
- alias :eql? :==
663
+ alias eql? ==
662
664
  end
663
665
 
664
666
  # Helper class to wrap the previous members of a referenced collection and
@@ -767,6 +769,7 @@ class ViewModel::ActiveRecord
767
769
  member.ref_string = ref_string if ref_string
768
770
  member
769
771
  end
772
+
770
773
  def remove_from_members(removed_members)
771
774
  s = removed_members.to_set
772
775
  members.reject! { |m| s.include?(m) }
@@ -900,11 +903,12 @@ class ViewModel::ActiveRecord
900
903
 
901
904
  def clear_association_cache(model, reflection)
902
905
  association = model.association(reflection.name)
903
- if reflection.collection?
904
- association.target = []
905
- else
906
- association.target = nil
907
- end
906
+ association.target =
907
+ if reflection.collection?
908
+ []
909
+ else
910
+ nil
911
+ end
908
912
  end
909
913
 
910
914
  def blame_reference
@@ -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
@@ -38,7 +38,6 @@ module ViewModel::AfterTransactionRunner
38
38
  end
39
39
  end
40
40
 
41
-
42
41
  # Override to tie to a specific connection.
43
42
  def connection
44
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
@@ -9,6 +9,7 @@ class ViewModel::Migration::NoPathError < ViewModel::AbstractError
9
9
  @vm_name = viewmodel.view_name
10
10
  @from = from
11
11
  @to = to
12
+ super()
12
13
  end
13
14
 
14
15
  def detail
@@ -8,6 +8,7 @@ class ViewModel::Migration::OneWayError < ViewModel::AbstractError
8
8
  def initialize(vm_name, direction)
9
9
  @vm_name = vm_name
10
10
  @direction = direction
11
+ super()
11
12
  end
12
13
 
13
14
  def detail
@@ -8,6 +8,7 @@ class ViewModel::Migration::UnspecifiedVersionError < ViewModel::AbstractError
8
8
  def initialize(vm_name, version)
9
9
  @vm_name = vm_name
10
10
  @version = version
11
+ super()
11
12
  end
12
13
 
13
14
  def detail
@@ -116,7 +116,7 @@ class ViewModel::Record < ViewModel
116
116
  end
117
117
  end
118
118
 
119
- def resolve_viewmodel(metadata, view_hash, deserialize_context:)
119
+ def resolve_viewmodel(_metadata, _view_hash, deserialize_context:)
120
120
  self.for_new_model
121
121
  end
122
122
 
@@ -181,6 +181,8 @@ class ViewModel::Record < ViewModel
181
181
  @changed_attributes = []
182
182
  @changed_nested_children = false
183
183
  @changed_referenced_children = false
184
+
185
+ super()
184
186
  end
185
187
 
186
188
  # VM::Record identity matches the identity of its model. If the model has a
@@ -309,12 +311,10 @@ class ViewModel::Record < ViewModel
309
311
  # viewmodel), it's only desired for converting the value to and from wire
310
312
  # format, so conversion is deferred to serialization time.
311
313
  value = attr_data.map_value(value) do |v|
312
- begin
313
- attr_data.attribute_serializer.dump(v, json: true)
314
- rescue IknowParams::Serializer::DumpError => ex
315
- raise ViewModel::SerializationError.new(
316
- "Could not serialize invalid value '#{vm_attr_name}': #{ex.message}")
317
- end
314
+ attr_data.attribute_serializer.dump(v, json: true)
315
+ rescue IknowParams::Serializer::DumpError => ex
316
+ raise ViewModel::SerializationError.new(
317
+ "Could not serialize invalid value '#{vm_attr_name}': #{ex.message}")
318
318
  end
319
319
  end
320
320
 
@@ -342,12 +342,10 @@ class ViewModel::Record < ViewModel
342
342
  end
343
343
  when attr_data.using_serializer?
344
344
  attr_data.map_value(serialized_value) do |sv|
345
- begin
346
- attr_data.attribute_serializer.load(sv)
347
- rescue IknowParams::Serializer::LoadError => ex
348
- reason = "could not be deserialized because #{ex.message}"
349
- raise ViewModel::DeserializationError::Validation.new(vm_attr_name, reason, {}, blame_reference)
350
- end
345
+ attr_data.attribute_serializer.load(sv)
346
+ rescue IknowParams::Serializer::LoadError => ex
347
+ reason = "could not be deserialized because #{ex.message}"
348
+ raise ViewModel::DeserializationError::Validation.new(vm_attr_name, reason, {}, blame_reference)
351
349
  end
352
350
  else
353
351
  serialized_value
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ViewModel
2
4
  # Key to identify a viewmodel with some kind of inherent ID (e.g. an ViewModel::ActiveRecord)
3
5
  class Reference
@@ -22,7 +24,7 @@ class ViewModel
22
24
  other.model_id == model_id
23
25
  end
24
26
 
25
- alias :eql? :==
27
+ alias eql? ==
26
28
 
27
29
  def hash
28
30
  [viewmodel_class, model_id].hash