iknow_view_models 2.8.4

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 (92) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +115 -0
  3. data/.gitignore +36 -0
  4. data/.travis.yml +31 -0
  5. data/Appraisals +9 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +19 -0
  9. data/Rakefile +21 -0
  10. data/appveyor.yml +22 -0
  11. data/gemfiles/rails_5_2.gemfile +15 -0
  12. data/gemfiles/rails_6_0_beta.gemfile +15 -0
  13. data/iknow_view_models.gemspec +49 -0
  14. data/lib/iknow_view_models.rb +12 -0
  15. data/lib/iknow_view_models/railtie.rb +8 -0
  16. data/lib/iknow_view_models/version.rb +5 -0
  17. data/lib/view_model.rb +333 -0
  18. data/lib/view_model/access_control.rb +154 -0
  19. data/lib/view_model/access_control/composed.rb +216 -0
  20. data/lib/view_model/access_control/open.rb +13 -0
  21. data/lib/view_model/access_control/read_only.rb +13 -0
  22. data/lib/view_model/access_control/tree.rb +264 -0
  23. data/lib/view_model/access_control_error.rb +10 -0
  24. data/lib/view_model/active_record.rb +383 -0
  25. data/lib/view_model/active_record/association_data.rb +178 -0
  26. data/lib/view_model/active_record/association_manipulation.rb +389 -0
  27. data/lib/view_model/active_record/cache.rb +265 -0
  28. data/lib/view_model/active_record/cache/cacheable_view.rb +51 -0
  29. data/lib/view_model/active_record/cloner.rb +113 -0
  30. data/lib/view_model/active_record/collection_nested_controller.rb +100 -0
  31. data/lib/view_model/active_record/controller.rb +77 -0
  32. data/lib/view_model/active_record/controller_base.rb +185 -0
  33. data/lib/view_model/active_record/nested_controller_base.rb +93 -0
  34. data/lib/view_model/active_record/singular_nested_controller.rb +34 -0
  35. data/lib/view_model/active_record/update_context.rb +252 -0
  36. data/lib/view_model/active_record/update_data.rb +749 -0
  37. data/lib/view_model/active_record/update_operation.rb +810 -0
  38. data/lib/view_model/active_record/visitor.rb +77 -0
  39. data/lib/view_model/after_transaction_runner.rb +29 -0
  40. data/lib/view_model/callbacks.rb +219 -0
  41. data/lib/view_model/changes.rb +62 -0
  42. data/lib/view_model/config.rb +29 -0
  43. data/lib/view_model/controller.rb +142 -0
  44. data/lib/view_model/deserialization_error.rb +437 -0
  45. data/lib/view_model/deserialize_context.rb +16 -0
  46. data/lib/view_model/error.rb +191 -0
  47. data/lib/view_model/error_view.rb +35 -0
  48. data/lib/view_model/record.rb +367 -0
  49. data/lib/view_model/record/attribute_data.rb +48 -0
  50. data/lib/view_model/reference.rb +31 -0
  51. data/lib/view_model/references.rb +48 -0
  52. data/lib/view_model/registry.rb +73 -0
  53. data/lib/view_model/schemas.rb +45 -0
  54. data/lib/view_model/serialization_error.rb +10 -0
  55. data/lib/view_model/serialize_context.rb +118 -0
  56. data/lib/view_model/test_helpers.rb +103 -0
  57. data/lib/view_model/test_helpers/arvm_builder.rb +111 -0
  58. data/lib/view_model/traversal_context.rb +126 -0
  59. data/lib/view_model/utils.rb +24 -0
  60. data/lib/view_model/utils/collections.rb +49 -0
  61. data/test/helpers/arvm_test_models.rb +59 -0
  62. data/test/helpers/arvm_test_utilities.rb +187 -0
  63. data/test/helpers/callback_tracer.rb +27 -0
  64. data/test/helpers/controller_test_helpers.rb +270 -0
  65. data/test/helpers/match_enumerator.rb +58 -0
  66. data/test/helpers/query_logging.rb +71 -0
  67. data/test/helpers/test_access_control.rb +56 -0
  68. data/test/helpers/viewmodel_spec_helpers.rb +326 -0
  69. data/test/unit/view_model/access_control_test.rb +769 -0
  70. data/test/unit/view_model/active_record/alias_test.rb +35 -0
  71. data/test/unit/view_model/active_record/belongs_to_test.rb +376 -0
  72. data/test/unit/view_model/active_record/cache_test.rb +351 -0
  73. data/test/unit/view_model/active_record/cloner_test.rb +313 -0
  74. data/test/unit/view_model/active_record/controller_test.rb +561 -0
  75. data/test/unit/view_model/active_record/counter_test.rb +80 -0
  76. data/test/unit/view_model/active_record/customization_test.rb +388 -0
  77. data/test/unit/view_model/active_record/has_many_test.rb +957 -0
  78. data/test/unit/view_model/active_record/has_many_through_poly_test.rb +269 -0
  79. data/test/unit/view_model/active_record/has_many_through_test.rb +736 -0
  80. data/test/unit/view_model/active_record/has_one_test.rb +334 -0
  81. data/test/unit/view_model/active_record/namespacing_test.rb +75 -0
  82. data/test/unit/view_model/active_record/optional_attribute_view_test.rb +58 -0
  83. data/test/unit/view_model/active_record/poly_test.rb +320 -0
  84. data/test/unit/view_model/active_record/shared_test.rb +285 -0
  85. data/test/unit/view_model/active_record/version_test.rb +121 -0
  86. data/test/unit/view_model/active_record_test.rb +542 -0
  87. data/test/unit/view_model/callbacks_test.rb +582 -0
  88. data/test/unit/view_model/deserialization_error/unique_violation_test.rb +73 -0
  89. data/test/unit/view_model/record_test.rb +524 -0
  90. data/test/unit/view_model/traversal_context_test.rb +371 -0
  91. data/test/unit/view_model_test.rb +62 -0
  92. metadata +490 -0
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CallbackTracer
4
+ include ViewModel::Callbacks
5
+
6
+ Visit = Struct.new(:hook, :view) do
7
+ def inspect
8
+ "#{hook.name}(#{view.to_reference})"
9
+ end
10
+ end
11
+
12
+ attr_reader :hook_trace
13
+
14
+ def initialize
15
+ @hook_trace = []
16
+ end
17
+
18
+ ViewModel::Callbacks::Hook.each do |hook|
19
+ send(hook.dsl_add_hook_name) do
20
+ hook_trace << Visit.new(hook, view)
21
+ end
22
+ end
23
+
24
+ def log!
25
+ puts hook_trace.map { |t| [t.hook.name, t.view.class, t.view.model].inspect }
26
+ end
27
+ end
@@ -0,0 +1,270 @@
1
+ require "iknow_view_models"
2
+ require "view_model/active_record"
3
+ require "view_model/active_record/controller"
4
+
5
+ require_relative "../helpers/arvm_test_utilities.rb"
6
+ require_relative "../helpers/arvm_test_models.rb"
7
+
8
+ require "acts_as_manual_list"
9
+
10
+ # models for ARVM controller test
11
+ module ControllerTestModels
12
+ def before_all
13
+ super
14
+
15
+ build_viewmodel(:Label) do
16
+ define_schema do |t|
17
+ t.string :text
18
+ end
19
+ define_model do
20
+ has_one :parent
21
+ has_one :target
22
+ end
23
+ define_viewmodel do
24
+ attributes :text
25
+ end
26
+ end
27
+
28
+ build_viewmodel(:Category) do
29
+ define_schema do |t|
30
+ t.string :name
31
+ end
32
+ define_model do
33
+ has_many :parents
34
+ end
35
+ define_viewmodel do
36
+ attributes :name
37
+ end
38
+ end
39
+
40
+ build_viewmodel(:PolyOne) do
41
+ define_schema do |t|
42
+ t.integer :number
43
+ end
44
+ define_model do
45
+ has_one :parent, as: :poly
46
+ end
47
+ define_viewmodel do
48
+ attributes :number
49
+ end
50
+ end
51
+
52
+ build_viewmodel(:PolyTwo) do
53
+ define_schema do |t|
54
+ t.string :text
55
+ end
56
+ define_model do
57
+ has_one :parent, as: :poly
58
+ end
59
+ define_viewmodel do
60
+ attributes :text
61
+ end
62
+ end
63
+
64
+ build_viewmodel(:Parent) do
65
+ define_schema do |t|
66
+ t.string :name
67
+ t.references :label, foreign_key: true
68
+ t.string :poly_type
69
+ t.integer :poly_id
70
+ t.references :category, foreign_key: true # shared reference
71
+ end
72
+ define_model do
73
+ has_many :children, dependent: :destroy, inverse_of: :parent
74
+ belongs_to :label, dependent: :destroy
75
+ has_one :target, dependent: :destroy, inverse_of: :parent
76
+ belongs_to :poly, polymorphic: true, dependent: :destroy, inverse_of: :parent
77
+ belongs_to :category
78
+ end
79
+ define_viewmodel do
80
+ attributes :name
81
+ associations :label, :target
82
+ association :children, optional: true
83
+ association :poly, viewmodels: [:PolyOne, :PolyTwo]
84
+ association :category, shared: true
85
+ end
86
+ end
87
+
88
+ build_viewmodel(:Child) do
89
+ define_schema do |t|
90
+ t.references :parent, null: false, foreign_key: true
91
+ t.string :name
92
+ t.float :position
93
+ end
94
+ # Add age column separately in order to define CHECK constraint (no way to
95
+ # specify in activerecord schema.
96
+ ActiveRecord::Base.connection.execute(<<-SQL)
97
+ ALTER TABLE children ADD COLUMN age integer CHECK(age > 21)
98
+ SQL
99
+ define_model do
100
+ belongs_to :parent, inverse_of: :children
101
+ acts_as_manual_list scope: :parent
102
+ validates :age, numericality: {less_than: 42}, allow_nil: true
103
+ end
104
+ define_viewmodel do
105
+ attributes :name, :age
106
+ acts_as_list :position
107
+ end
108
+ end
109
+
110
+ build_viewmodel(:Target) do
111
+ define_schema do |t|
112
+ t.string :text
113
+ t.references :parent, foreign_key: true
114
+ t.references :label, foreign_key: true
115
+ end
116
+ define_model do
117
+ belongs_to :parent, inverse_of: :target
118
+ belongs_to :label, dependent: :destroy
119
+ end
120
+ define_viewmodel do
121
+ attributes :text
122
+ association :label
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ ## Dummy Rails Controllers
129
+ class DummyController
130
+ attr_reader :params, :status
131
+
132
+ def initialize(**params)
133
+ # in Rails 5, this will not be a hash, which weakens the value of the test.
134
+ @params = params.with_indifferent_access
135
+ @status = 200
136
+ end
137
+
138
+ def invoke(method)
139
+ begin
140
+ self.public_send(method)
141
+ rescue Exception => ex
142
+ handler = self.class.rescue_block(ex.class)
143
+ case handler
144
+ when nil
145
+ raise
146
+ when Symbol
147
+ self.send(handler, ex)
148
+ when Proc
149
+ self.instance_exec(ex, &handler)
150
+ end
151
+ end
152
+ end
153
+
154
+ def invoke_without_rescue(method)
155
+ self.public_send(method)
156
+ end
157
+
158
+ def render(status:, **options)
159
+ if options.has_key?(:json)
160
+ @response_body = options[:json]
161
+ @content_type = options[:content_type] || 'application/json'
162
+ elsif options.has_key?(:plain)
163
+ @response_body = options[:plain]
164
+ @content_type = options[:content_type] || 'text/plain'
165
+ end
166
+ @status = status unless status.nil?
167
+ end
168
+
169
+ def json_response
170
+ raise "Not a JSON response" unless @content_type == 'application/json'
171
+ @response_body
172
+ end
173
+
174
+ def hash_response
175
+ JSON.parse(json_response)
176
+ end
177
+
178
+ class << self
179
+ def inherited(subclass)
180
+ subclass.initialize_rescue_blocks
181
+ end
182
+
183
+ def initialize_rescue_blocks
184
+ @rescue_blocks = {}
185
+ end
186
+
187
+ def rescue_from(type, with:)
188
+ @rescue_blocks[type] = with
189
+ end
190
+
191
+ def rescue_block(type)
192
+ @rescue_blocks.to_a.reverse.detect { |btype, h| type <= btype }.try(&:last)
193
+ end
194
+
195
+ def etag(*)
196
+ end
197
+ end
198
+ end
199
+
200
+ # Provide dummy Rails env
201
+ module Rails
202
+ def self.env
203
+ 'production'
204
+ end
205
+ end
206
+
207
+ module ActionController
208
+ class Parameters
209
+ end
210
+ end
211
+
212
+ module CallbackTracing
213
+ attr_reader :callback_tracer
214
+ delegate :hook_trace, to: :callback_tracer
215
+
216
+ def new_deserialize_context(**args)
217
+ @callback_tracer ||= CallbackTracer.new
218
+ super(callbacks: [@callback_tracer])
219
+ end
220
+
221
+ def new_serialize_context(**args)
222
+ @callback_tracer ||= CallbackTracer.new
223
+ super(callbacks: [@callback_tracer])
224
+ end
225
+ end
226
+
227
+ module ControllerTestControllers
228
+ def before_all
229
+ super
230
+
231
+ Class.new(DummyController) do |c|
232
+ Object.const_set(:ParentController, self)
233
+ include ViewModel::ActiveRecord::Controller
234
+ include CallbackTracing
235
+ self.access_control = ViewModel::AccessControl::Open
236
+ end
237
+
238
+ Class.new(DummyController) do |c|
239
+ Object.const_set(:ChildController, self)
240
+ include ViewModel::ActiveRecord::Controller
241
+ include CallbackTracing
242
+ self.access_control = ViewModel::AccessControl::Open
243
+ nested_in :parent, as: :children
244
+ end
245
+
246
+ Class.new(DummyController) do |c|
247
+ Object.const_set(:LabelController, self)
248
+ include ViewModel::ActiveRecord::Controller
249
+ include CallbackTracing
250
+ self.access_control = ViewModel::AccessControl::Open
251
+ nested_in :parent, as: :label
252
+ end
253
+
254
+ Class.new(DummyController) do |c|
255
+ Object.const_set(:TargetController, self)
256
+ include ViewModel::ActiveRecord::Controller
257
+ include CallbackTracing
258
+ self.access_control = ViewModel::AccessControl::Open
259
+ nested_in :parent, as: :target
260
+ end
261
+ end
262
+
263
+ def after_all
264
+ [:ParentController, :ChildController, :LabelController, :TargetController].each do |name|
265
+ Object.send(:remove_const, name)
266
+ end
267
+ ActiveSupport::Dependencies::Reference.clear!
268
+ super
269
+ end
270
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ # From https://stackoverflow.com/a/41293357
4
+ module MiniTest::Assertions
5
+ class MatchEnumerator
6
+ def initialize(expected, actual)
7
+ @expected = expected
8
+ @actual = actual
9
+ end
10
+
11
+ def match
12
+ return result, message
13
+ end
14
+
15
+ def result
16
+ return false unless @actual.respond_to? :to_a
17
+ @extra_items = difference_between_enumerators(@actual, @expected)
18
+ @missing_items = difference_between_enumerators(@expected, @actual)
19
+ @extra_items.empty? & @missing_items.empty?
20
+ end
21
+
22
+ def message
23
+ if @actual.respond_to? :to_a
24
+ message = "expected collection contained: #{safe_sort(@expected).inspect}\n"
25
+ message += "actual collection contained: #{safe_sort(@actual).inspect}\n"
26
+ message += "the missing elements were: #{safe_sort(@missing_items).inspect}\n" unless @missing_items.empty?
27
+ message += "the extra elements were: #{safe_sort(@extra_items).inspect}\n" unless @extra_items.empty?
28
+ else
29
+ message = "expected an array, actual collection was #{@actual.inspect}"
30
+ end
31
+
32
+ message
33
+ end
34
+
35
+ private
36
+
37
+ def safe_sort(array)
38
+ array.sort rescue array
39
+ end
40
+
41
+ def difference_between_enumerators(array_1, array_2)
42
+ difference = array_1.to_a.dup
43
+ array_2.to_a.each do |element|
44
+ if (index = difference.index(element))
45
+ difference.delete_at(index)
46
+ end
47
+ end
48
+ difference
49
+ end
50
+ end # MatchEnumerator
51
+
52
+ def assert_match_enumerator(expected, actual)
53
+ result, message = MatchEnumerator.new(expected, actual).match
54
+ assert result, message
55
+ end
56
+ end # MiniTest::Assertions
57
+
58
+ Enumerator.infect_an_assertion :assert_match_enumerator, :must_contain_exactly
@@ -0,0 +1,71 @@
1
+ # Test mixin that allows queries executed in a block to be introspected.
2
+ #
3
+ # Code run within a `log_queries` block will collect data. Collected data is
4
+ # inspected via `logged_queries` which returns everything, or via
5
+ # `logged_named_queries`, which returns only valid payload names.
6
+ #
7
+ # Caveats: only supports single threaded testing.
8
+
9
+ require 'active_support/subscriber'
10
+
11
+ module QueryLogging
12
+
13
+ # ActiveRecord integration
14
+ class QueryLogger < ActiveSupport::Subscriber
15
+ @log = false
16
+ @query_log = []
17
+
18
+ attach_to :active_record
19
+
20
+ def self.clear!
21
+ @query_log = []
22
+ end
23
+
24
+ def self.with_query_log
25
+ clear!
26
+ @log = true
27
+ yield
28
+ ensure
29
+ @log = false
30
+ end
31
+
32
+ def self.log?
33
+ @log
34
+ end
35
+
36
+ def self.logged_events
37
+ @query_log
38
+ end
39
+
40
+ # All public methods are event handlers. The instance defines what to log,
41
+ # while the class defines how to handle it.
42
+
43
+ def sql(event)
44
+ if self.class.log?
45
+ self.class.logged_events << event
46
+ end
47
+ end
48
+ end
49
+
50
+ # Defensively clean up before every test.
51
+ def setup
52
+ super
53
+ QueryLogger.clear!
54
+ end
55
+
56
+ # Test helpers
57
+
58
+ def log_queries
59
+ QueryLogger.with_query_log { yield }
60
+ end
61
+
62
+ def logged_queries
63
+ QueryLogger.logged_events
64
+ end
65
+
66
+ def logged_load_queries
67
+ QueryLogger.logged_events
68
+ .map { |x| x.payload[:name] }
69
+ .select { |x| x && x =~ / Load$/ }
70
+ end
71
+ end
@@ -0,0 +1,56 @@
1
+ require "iknow_view_models"
2
+
3
+
4
+ class TestAccessControl < ViewModel::AccessControl
5
+ attr_accessor :editable_checks, :visible_checks
6
+
7
+ def initialize(can_view, can_edit, can_change)
8
+ super()
9
+ @can_edit = can_edit
10
+ @can_view = can_view
11
+ @can_change = can_change
12
+ @editable_checks = []
13
+ @visible_checks = []
14
+ @valid_edit_checks = []
15
+ end
16
+
17
+ # Collect
18
+
19
+ def editable_check(traversal_env)
20
+ @editable_checks << traversal_env.view.to_reference
21
+ ViewModel::AccessControl::Result.new(@can_edit)
22
+ end
23
+
24
+ def valid_edit_check(traversal_env)
25
+ ref = traversal_env.view.to_reference
26
+ @valid_edit_checks << [ref, traversal_env.changes]
27
+ ViewModel::AccessControl::Result.new(@can_change)
28
+ end
29
+
30
+ def visible_check(traversal_env)
31
+ @visible_checks << traversal_env.view.to_reference
32
+ ViewModel::AccessControl::Result.new(@can_view)
33
+ end
34
+
35
+ # Query (also see attr_accessors)
36
+
37
+ def valid_edit_refs
38
+ @valid_edit_checks.map { |ref, _changes| ref }
39
+ end
40
+
41
+ def valid_edit_changes(ref)
42
+ all = all_valid_edit_changes(ref)
43
+ raise "Expected single change for ref '#{ref}'; found #{all}" unless all.size == 1
44
+ all.first
45
+ end
46
+
47
+ def all_valid_edit_changes(ref)
48
+ @valid_edit_checks
49
+ .select { | cref, _changes| cref == ref }
50
+ .map { |_cref, changes| changes }
51
+ end
52
+
53
+ def was_edited?(ref)
54
+ all_valid_edit_changes(ref).present?
55
+ end
56
+ end