iknow_view_models 2.8.4

Sign up to get free protection for your applications and to get access to all the features.
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