cuprum-rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +98 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/DEVELOPMENT.md +28 -0
  5. data/LICENSE +22 -0
  6. data/README.md +1045 -0
  7. data/lib/cuprum/rails/action.rb +45 -0
  8. data/lib/cuprum/rails/actions/create.rb +49 -0
  9. data/lib/cuprum/rails/actions/destroy.rb +22 -0
  10. data/lib/cuprum/rails/actions/edit.rb +22 -0
  11. data/lib/cuprum/rails/actions/index.rb +55 -0
  12. data/lib/cuprum/rails/actions/new.rb +19 -0
  13. data/lib/cuprum/rails/actions/resource_action.rb +75 -0
  14. data/lib/cuprum/rails/actions/show.rb +22 -0
  15. data/lib/cuprum/rails/actions/update.rb +59 -0
  16. data/lib/cuprum/rails/actions.rb +16 -0
  17. data/lib/cuprum/rails/collection.rb +115 -0
  18. data/lib/cuprum/rails/command.rb +137 -0
  19. data/lib/cuprum/rails/commands/assign_one.rb +66 -0
  20. data/lib/cuprum/rails/commands/build_one.rb +55 -0
  21. data/lib/cuprum/rails/commands/destroy_one.rb +43 -0
  22. data/lib/cuprum/rails/commands/find_many.rb +60 -0
  23. data/lib/cuprum/rails/commands/find_matching.rb +121 -0
  24. data/lib/cuprum/rails/commands/find_one.rb +50 -0
  25. data/lib/cuprum/rails/commands/insert_one.rb +41 -0
  26. data/lib/cuprum/rails/commands/update_one.rb +49 -0
  27. data/lib/cuprum/rails/commands/validate_one.rb +68 -0
  28. data/lib/cuprum/rails/commands.rb +18 -0
  29. data/lib/cuprum/rails/controller.rb +50 -0
  30. data/lib/cuprum/rails/controller_action.rb +121 -0
  31. data/lib/cuprum/rails/controllers/class_methods/actions.rb +57 -0
  32. data/lib/cuprum/rails/controllers/class_methods/configuration.rb +64 -0
  33. data/lib/cuprum/rails/controllers/class_methods/validations.rb +30 -0
  34. data/lib/cuprum/rails/controllers/class_methods.rb +15 -0
  35. data/lib/cuprum/rails/controllers/configuration.rb +53 -0
  36. data/lib/cuprum/rails/controllers.rb +10 -0
  37. data/lib/cuprum/rails/errors/missing_parameters.rb +33 -0
  38. data/lib/cuprum/rails/errors/missing_primary_key.rb +46 -0
  39. data/lib/cuprum/rails/errors/undefined_permitted_attributes.rb +34 -0
  40. data/lib/cuprum/rails/errors.rb +8 -0
  41. data/lib/cuprum/rails/map_errors.rb +44 -0
  42. data/lib/cuprum/rails/query.rb +77 -0
  43. data/lib/cuprum/rails/query_builder.rb +78 -0
  44. data/lib/cuprum/rails/repository.rb +44 -0
  45. data/lib/cuprum/rails/request.rb +105 -0
  46. data/lib/cuprum/rails/resource.rb +145 -0
  47. data/lib/cuprum/rails/responders/actions.rb +73 -0
  48. data/lib/cuprum/rails/responders/html/plural_resource.rb +62 -0
  49. data/lib/cuprum/rails/responders/html/singular_resource.rb +59 -0
  50. data/lib/cuprum/rails/responders/html.rb +11 -0
  51. data/lib/cuprum/rails/responders/html_responder.rb +129 -0
  52. data/lib/cuprum/rails/responders/json/resource.rb +60 -0
  53. data/lib/cuprum/rails/responders/json.rb +10 -0
  54. data/lib/cuprum/rails/responders/json_responder.rb +122 -0
  55. data/lib/cuprum/rails/responders/matching.rb +145 -0
  56. data/lib/cuprum/rails/responders/serialization.rb +36 -0
  57. data/lib/cuprum/rails/responders.rb +15 -0
  58. data/lib/cuprum/rails/responses/html/redirect_response.rb +29 -0
  59. data/lib/cuprum/rails/responses/html/render_response.rb +52 -0
  60. data/lib/cuprum/rails/responses/html.rb +11 -0
  61. data/lib/cuprum/rails/responses/json_response.rb +29 -0
  62. data/lib/cuprum/rails/responses.rb +11 -0
  63. data/lib/cuprum/rails/routes.rb +166 -0
  64. data/lib/cuprum/rails/routing/plural_routes.rb +26 -0
  65. data/lib/cuprum/rails/routing/singular_routes.rb +24 -0
  66. data/lib/cuprum/rails/routing.rb +11 -0
  67. data/lib/cuprum/rails/rspec/command_contract.rb +460 -0
  68. data/lib/cuprum/rails/rspec/define_route_contract.rb +84 -0
  69. data/lib/cuprum/rails/rspec.rb +8 -0
  70. data/lib/cuprum/rails/serializers/json/active_record_serializer.rb +24 -0
  71. data/lib/cuprum/rails/serializers/json/array_serializer.rb +40 -0
  72. data/lib/cuprum/rails/serializers/json/attributes_serializer.rb +217 -0
  73. data/lib/cuprum/rails/serializers/json/error_serializer.rb +24 -0
  74. data/lib/cuprum/rails/serializers/json/hash_serializer.rb +44 -0
  75. data/lib/cuprum/rails/serializers/json/identity_serializer.rb +21 -0
  76. data/lib/cuprum/rails/serializers/json/serializer.rb +66 -0
  77. data/lib/cuprum/rails/serializers/json.rb +40 -0
  78. data/lib/cuprum/rails/serializers.rb +10 -0
  79. data/lib/cuprum/rails/version.rb +59 -0
  80. data/lib/cuprum/rails.rb +31 -0
  81. metadata +286 -0
@@ -0,0 +1,460 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/rspec'
4
+
5
+ require 'support/book'
6
+ require 'support/tome'
7
+
8
+ module Cuprum::Rails::RSpec
9
+ # Contract validating the behavior of a Rails command implementation.
10
+ COMMAND_CONTRACT = lambda do
11
+ describe '.subclass' do
12
+ let(:subclass) { described_class.subclass }
13
+ let(:constructor_options) do
14
+ {
15
+ record_class: Book,
16
+ optional_key: 'optional value'
17
+ }
18
+ end
19
+
20
+ it 'should define the class method' do
21
+ expect(described_class)
22
+ .to respond_to(:subclass)
23
+ .with(0).arguments
24
+ .and_any_keywords
25
+ end
26
+
27
+ it { expect(subclass).to be_a Class }
28
+
29
+ it { expect(subclass).to be < described_class }
30
+
31
+ it 'should define the constructor' do
32
+ expect(subclass)
33
+ .to respond_to(:new)
34
+ .with(0).arguments
35
+ .and_any_keywords
36
+ end
37
+
38
+ it 'should return the record class' do
39
+ expect(subclass.new(**constructor_options).record_class)
40
+ .to be record_class
41
+ end
42
+
43
+ it 'should return the options' do
44
+ expect(subclass.new(**constructor_options).options)
45
+ .to be == { optional_key: 'optional value' }
46
+ end
47
+
48
+ describe 'with options' do
49
+ let(:default_options) do
50
+ {
51
+ record_class: Book,
52
+ custom_key: 'custom value'
53
+ }
54
+ end
55
+ let(:constructor_options) do
56
+ {
57
+ optional_key: 'optional value'
58
+ }
59
+ end
60
+ let(:subclass) { described_class.subclass(**default_options) }
61
+
62
+ it { expect(subclass).to be_a Class }
63
+
64
+ it { expect(subclass).to be < described_class }
65
+
66
+ it 'should define the constructor' do
67
+ expect(subclass)
68
+ .to respond_to(:new)
69
+ .with(0).arguments
70
+ .and_any_keywords
71
+ end
72
+
73
+ it 'should return the record class' do
74
+ expect(subclass.new(**constructor_options).record_class)
75
+ .to be record_class
76
+ end
77
+
78
+ it 'should return the options' do
79
+ expect(subclass.new(**constructor_options).options)
80
+ .to be == {
81
+ custom_key: 'custom value',
82
+ optional_key: 'optional value'
83
+ }
84
+ end
85
+ end
86
+ end
87
+
88
+ describe '#collection_name' do
89
+ let(:expected) { record_class.name.underscore.pluralize }
90
+
91
+ include_examples 'should define reader',
92
+ :collection_name,
93
+ -> { be == expected }
94
+
95
+ context 'when initialized with collection_name: string' do
96
+ let(:collection_name) { 'books' }
97
+ let(:constructor_options) do
98
+ super().merge(collection_name: collection_name)
99
+ end
100
+
101
+ it { expect(command.collection_name).to be == collection_name }
102
+ end
103
+
104
+ context 'when initialized with collection_name: symbol' do
105
+ let(:collection_name) { :books }
106
+ let(:constructor_options) do
107
+ super().merge(collection_name: collection_name)
108
+ end
109
+
110
+ it { expect(command.collection_name).to be == collection_name.to_s }
111
+ end
112
+ end
113
+
114
+ describe '#member_name' do
115
+ def tools
116
+ SleepingKingStudios::Tools::Toolbelt.instance
117
+ end
118
+
119
+ include_examples 'should have reader',
120
+ :member_name,
121
+ -> { record_class.name.underscore }
122
+
123
+ context 'when initialized with collection_name: value' do
124
+ let(:collection_name) { :books }
125
+
126
+ it 'should return the singular collection name' do
127
+ expect(command.member_name)
128
+ .to be == tools.str.singularize(collection_name.to_s)
129
+ end
130
+ end
131
+
132
+ context 'when initialized with member_name: string' do
133
+ let(:member_name) { 'tome' }
134
+ let(:constructor_options) { super().merge(member_name: member_name) }
135
+
136
+ it 'should return the singular collection name' do
137
+ expect(command.member_name).to be member_name
138
+ end
139
+ end
140
+
141
+ context 'when initialized with member_name: symbol' do
142
+ let(:member_name) { :tome }
143
+ let(:constructor_options) { super().merge(member_name: member_name) }
144
+
145
+ it 'should return the singular collection name' do
146
+ expect(command.member_name).to be == member_name.to_s
147
+ end
148
+ end
149
+ end
150
+
151
+ describe '#options' do
152
+ let(:expected_options) do
153
+ defined?(super()) ? super() : constructor_options
154
+ end
155
+
156
+ include_examples 'should define reader',
157
+ :options,
158
+ -> { be == expected_options }
159
+
160
+ context 'when initialized with options' do
161
+ let(:constructor_options) { super().merge({ key: 'value' }) }
162
+ let(:expected_options) { super().merge({ key: 'value' }) }
163
+
164
+ it { expect(command.options).to be == expected_options }
165
+ end
166
+ end
167
+
168
+ describe '#primary_key_name' do
169
+ include_examples 'should define reader', :primary_key_name, :id
170
+
171
+ context 'with a record class with custom primary key' do
172
+ let(:record_class) { Tome }
173
+
174
+ include_examples 'should define reader', :primary_key_name, :uuid
175
+ end
176
+ end
177
+
178
+ describe '#primary_key_type' do
179
+ include_examples 'should define reader', :primary_key_type, Integer
180
+
181
+ context 'with a record class with custom primary key' do
182
+ let(:record_class) { Tome }
183
+
184
+ include_examples 'should define reader', :primary_key_type, String
185
+ end
186
+ end
187
+
188
+ describe '#record_class' do
189
+ include_examples 'should define reader',
190
+ :record_class,
191
+ -> { record_class }
192
+ end
193
+
194
+ describe '#validate_entity' do
195
+ let(:expected_error) do
196
+ type = record_class
197
+ contract = Stannum::Contracts::ParametersContract.new do
198
+ keyword :entity, type
199
+ end
200
+ errors = contract.errors_for(
201
+ {
202
+ arguments: [],
203
+ block: nil,
204
+ keywords: { entity: nil }
205
+ }
206
+ )
207
+
208
+ Cuprum::Collections::Errors::InvalidParameters.new(
209
+ command: command,
210
+ errors: errors
211
+ )
212
+ end
213
+
214
+ it 'should define the private method' do
215
+ expect(command)
216
+ .to respond_to(:validate_entity, true)
217
+ .with(1).argument
218
+ end
219
+
220
+ describe 'with nil' do
221
+ it 'should return a failing result' do
222
+ expect(command.send(:validate_entity, nil))
223
+ .to be_a_failing_result
224
+ .with_error(expected_error)
225
+ end
226
+ end
227
+
228
+ describe 'with an Object' do
229
+ it 'should return a failing result' do
230
+ expect(command.send(:validate_entity, Object.new.freeze))
231
+ .to be_a_failing_result
232
+ .with_error(expected_error)
233
+ end
234
+ end
235
+
236
+ describe 'with an invalid record instance' do
237
+ it 'should return a failing result' do
238
+ expect(command.send(:validate_entity, Tome.new))
239
+ .to be_a_failing_result
240
+ .with_error(expected_error)
241
+ end
242
+ end
243
+
244
+ describe 'with a valid record instance' do
245
+ it 'should not return a result' do
246
+ expect(command.send(:validate_entity, Book.new))
247
+ .not_to be_a_result
248
+ end
249
+ end
250
+ end
251
+
252
+ describe '#validate_primary_key' do
253
+ let(:primary_key_type) { Integer }
254
+ let(:expected_error) do
255
+ type = primary_key_type
256
+ contract = Stannum::Contracts::ParametersContract.new do
257
+ keyword :primary_key, type
258
+ end
259
+ errors = contract.errors_for(
260
+ {
261
+ arguments: [],
262
+ block: nil,
263
+ keywords: { primary_key: nil }
264
+ }
265
+ )
266
+
267
+ Cuprum::Collections::Errors::InvalidParameters.new(
268
+ command: command,
269
+ errors: errors
270
+ )
271
+ end
272
+
273
+ it 'should define the private method' do
274
+ expect(command)
275
+ .to respond_to(:validate_primary_key, true)
276
+ .with(1).argument
277
+ end
278
+
279
+ describe 'with nil' do
280
+ it 'should return a failing result' do
281
+ expect(command.send(:validate_primary_key, nil))
282
+ .to be_a_failing_result
283
+ .with_error(expected_error)
284
+ end
285
+ end
286
+
287
+ describe 'with an Object' do
288
+ it 'should return a failing result' do
289
+ expect(command.send(:validate_primary_key, Object.new.freeze))
290
+ .to be_a_failing_result
291
+ .with_error(expected_error)
292
+ end
293
+ end
294
+
295
+ describe 'with a String' do
296
+ it 'should return a failing result' do
297
+ expect(command.send(:validate_primary_key, '12345'))
298
+ .to be_a_failing_result
299
+ .with_error(expected_error)
300
+ end
301
+ end
302
+
303
+ describe 'with an Integer' do
304
+ it 'should not return a result' do
305
+ expect(command.send(:validate_primary_key, 12_345)).not_to be_a_result
306
+ end
307
+ end
308
+
309
+ context 'with a record class with custom primary key' do
310
+ let(:record_class) { Tome }
311
+ let(:primary_key_type) { String }
312
+
313
+ describe 'with an Integer' do
314
+ it 'should return a failing result' do
315
+ expect(command.send(:validate_primary_key, 12_345))
316
+ .to be_a_failing_result
317
+ .with_error(expected_error)
318
+ end
319
+ end
320
+
321
+ describe 'with a String' do
322
+ it 'should not return a result' do
323
+ expect(command.send(:validate_primary_key, '12345'))
324
+ .not_to be_a_result
325
+ end
326
+ end
327
+ end
328
+ end
329
+
330
+ describe '#validate_primary_keys' do
331
+ let(:primary_keys) { nil }
332
+ let(:primary_key_type) { Integer }
333
+ let(:expected_error) do
334
+ type = primary_key_type
335
+ contract = Stannum::Contracts::ParametersContract.new do
336
+ keyword :primary_keys,
337
+ Stannum::Constraints::Types::ArrayType.new(item_type: type)
338
+ end
339
+ errors = contract.errors_for(
340
+ {
341
+ arguments: [],
342
+ block: nil,
343
+ keywords: { primary_keys: primary_keys }
344
+ }
345
+ )
346
+
347
+ Cuprum::Collections::Errors::InvalidParameters.new(
348
+ command: command,
349
+ errors: errors
350
+ )
351
+ end
352
+
353
+ it 'should define the private method' do
354
+ expect(command)
355
+ .to respond_to(:validate_primary_keys, true)
356
+ .with(1).argument
357
+ end
358
+
359
+ describe 'with nil' do
360
+ it 'should return a failing result' do
361
+ expect(command.send(:validate_primary_keys, nil))
362
+ .to be_a_failing_result
363
+ .with_error(expected_error)
364
+ end
365
+ end
366
+
367
+ describe 'with an Object' do
368
+ it 'should return a failing result' do
369
+ expect(command.send(:validate_primary_keys, Object.new.freeze))
370
+ .to be_a_failing_result
371
+ .with_error(expected_error)
372
+ end
373
+ end
374
+
375
+ describe 'with a String' do
376
+ it 'should return a failing result' do
377
+ expect(command.send(:validate_primary_keys, '12345'))
378
+ .to be_a_failing_result
379
+ .with_error(expected_error)
380
+ end
381
+ end
382
+
383
+ describe 'with an Integer' do
384
+ it 'should return a failing result' do
385
+ expect(command.send(:validate_primary_keys, 12_345))
386
+ .to be_a_failing_result
387
+ .with_error(expected_error)
388
+ end
389
+ end
390
+
391
+ describe 'with an empty Array' do
392
+ it 'should not return a result' do
393
+ expect(command.send(:validate_primary_keys, []))
394
+ .not_to be_a_result
395
+ end
396
+ end
397
+
398
+ describe 'with an Array with nil values' do
399
+ let(:primary_keys) { Array.new(3, nil) }
400
+
401
+ it 'should return a failing result' do
402
+ expect(command.send(:validate_primary_keys, primary_keys))
403
+ .to be_a_failing_result
404
+ .with_error(expected_error)
405
+ end
406
+ end
407
+
408
+ describe 'with an Array with Object values' do
409
+ let(:primary_keys) { Array.new(3) { Object.new.freeze } }
410
+
411
+ it 'should return a failing result' do
412
+ expect(command.send(:validate_primary_keys, primary_keys))
413
+ .to be_a_failing_result
414
+ .with_error(expected_error)
415
+ end
416
+ end
417
+
418
+ describe 'with an Array with String values' do
419
+ let(:primary_keys) { %w[ichi ni san] }
420
+
421
+ it 'should return a failing result' do
422
+ expect(command.send(:validate_primary_keys, primary_keys))
423
+ .to be_a_failing_result
424
+ .with_error(expected_error)
425
+ end
426
+ end
427
+
428
+ describe 'with an Array with Integer values' do
429
+ it 'should not return a result' do
430
+ expect(command.send(:validate_primary_keys, [0, 1, 2]))
431
+ .not_to be_a_result
432
+ end
433
+ end
434
+
435
+ context 'with a record class with custom primary key' do
436
+ let(:record_class) { Tome }
437
+ let(:primary_key_type) { String }
438
+
439
+ describe 'with an Array with String values' do
440
+ let(:primary_keys) { %w[ichi ni san] }
441
+
442
+ it 'should not return a result' do
443
+ expect(command.send(:validate_primary_keys, primary_keys))
444
+ .not_to be_a_result
445
+ end
446
+ end
447
+
448
+ describe 'with an Array with Integer values' do
449
+ let(:primary_keys) { [0, 1, 2] }
450
+
451
+ it 'should return a failing result' do
452
+ expect(command.send(:validate_primary_keys, primary_keys))
453
+ .to be_a_failing_result
454
+ .with_error(expected_error)
455
+ end
456
+ end
457
+ end
458
+ end
459
+ end
460
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/rspec'
4
+
5
+ module Cuprum::Rails::RSpec
6
+ # Contract asserting that a Routes object defines the given route.
7
+ DEFINE_ROUTE_CONTRACT = lambda \
8
+ do |action_name:, path:, member_action: false, wildcards: {}|
9
+ describe "##{action_name}_path" do
10
+ let(:method_name) { "#{action_name}_path" }
11
+ let(:expected) do
12
+ next path unless member_action
13
+
14
+ primary_key_name = entity.class.primary_key
15
+ primary_key_value = entity[primary_key_name]
16
+
17
+ path.sub(':id', primary_key_value.to_s)
18
+ end
19
+
20
+ define_method(:route_helper) do
21
+ scoped = wildcards.empty? ? routes : routes.with_wildcards(wildcards)
22
+
23
+ if member_action
24
+ scoped.send(method_name, entity)
25
+ else
26
+ scoped.send(method_name)
27
+ end
28
+ end
29
+
30
+ it 'should define the helper method' do
31
+ if member_action
32
+ expect(routes).to respond_to(method_name).with(1).argument
33
+ else
34
+ expect(routes).to respond_to(method_name).with(0).arguments
35
+ end
36
+ end
37
+
38
+ it 'should return the route path' do
39
+ expect(route_helper).to be == expected
40
+ end
41
+
42
+ if member_action
43
+ describe 'when the entity is nil' do
44
+ let(:entity) { nil }
45
+ let(:error_message) { 'missing wildcard :id' }
46
+
47
+ it 'should raise an exception' do
48
+ expect { route_helper }
49
+ .to raise_error(
50
+ described_class::MissingWildcardError,
51
+ error_message
52
+ )
53
+ end
54
+ end
55
+ end
56
+
57
+ wildcards.each do |key, _|
58
+ wildcard = key.to_s.end_with?('_id') ? key.intern : :"#{key}_id"
59
+
60
+ describe "when the #{wildcard.inspect} wildcard is undefined" do
61
+ let(:error_message) { "missing wildcard #{wildcard.inspect}" }
62
+
63
+ define_method(:route_helper) do
64
+ scoped = routes.with_wildcards(wildcards.except(key))
65
+
66
+ if member_action
67
+ scoped.send(method_name, entity)
68
+ else
69
+ scoped.send(method_name)
70
+ end
71
+ end
72
+
73
+ it 'should raise an exception' do
74
+ expect { route_helper }
75
+ .to raise_error(
76
+ described_class::MissingWildcardError,
77
+ error_message
78
+ )
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails'
4
+
5
+ module Cuprum::Rails
6
+ # Namespace for RSpec contracts, which validate collection implementations.
7
+ module RSpec; end
8
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/serializers/json'
4
+ require 'cuprum/rails/serializers/json/serializer'
5
+
6
+ module Cuprum::Rails::Serializers::Json
7
+ # Converts ActiveRecord record to JSON using the #as_json method.
8
+ class ActiveRecordSerializer < Cuprum::Rails::Serializers::Json::Serializer
9
+ # Converts the ActiveRecord record to JSON.
10
+ #
11
+ # Calls and returns the #as_json method of the record.
12
+ #
13
+ # @param record [ActiveRecord::Base] The record to convert to JSON.
14
+ #
15
+ # @return [Hash] a JSON-compatible representation of the record.
16
+ def call(record, **_)
17
+ unless record.is_a?(ActiveRecord::Base)
18
+ raise ArgumentError, 'object must be an ActiveRecord record'
19
+ end
20
+
21
+ record.as_json
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/serializers/json'
4
+ require 'cuprum/rails/serializers/json/serializer'
5
+
6
+ module Cuprum::Rails::Serializers::Json
7
+ # Converts Array data structures to JSON based on configured serializers.
8
+ class ArraySerializer < Cuprum::Rails::Serializers::Json::Serializer
9
+ # Converts the array to JSON using the given serializers.
10
+ #
11
+ # First, #call finds the best serializer from the :serializers Hash for each
12
+ # item in the Array. This is done by walking up the object class's ancestors
13
+ # to find the closest ancestor which is a key in the :serializers Hash. The
14
+ # corresponding value is then called with the object, and the results are
15
+ # combined into a new Array and returned.
16
+ #
17
+ # @param array [Array] The array to convert to JSON.
18
+ # @param serializers [Hash<Class, #call>] The serializers for different
19
+ # object types.
20
+ #
21
+ # @return [Array] a JSON-compatible representation of the array.
22
+ #
23
+ # @raise UndefinedSerializerError if there is no matching serializer for
24
+ # any of the items in the array.
25
+ def call(array, serializers:)
26
+ raise ArgumentError, 'object must be an Array' unless array.is_a?(Array)
27
+
28
+ array.map { |item| super(item, serializers: serializers) }
29
+ end
30
+
31
+ private
32
+
33
+ def handle_recursion!(_object)
34
+ # Call serializes the items, not the array. Because the context changes,
35
+ # we don't need to check for recursion (unless the Array contains itself,
36
+ # in which case here there be dragons).
37
+ yield
38
+ end
39
+ end
40
+ end