grape 0.13.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of grape might be problematic. Click here for more details.

Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +9 -4
  3. data/CHANGELOG.md +28 -0
  4. data/Gemfile +0 -1
  5. data/Gemfile.lock +166 -0
  6. data/README.md +305 -163
  7. data/Rakefile +30 -33
  8. data/UPGRADING.md +31 -0
  9. data/benchmark/simple.rb +27 -0
  10. data/gemfiles/rack_1.5.2.gemfile +13 -0
  11. data/gemfiles/rails_3.gemfile +2 -2
  12. data/gemfiles/rails_4.gemfile +1 -2
  13. data/grape.gemspec +5 -4
  14. data/lib/grape.rb +9 -5
  15. data/lib/grape/dsl/configuration.rb +5 -2
  16. data/lib/grape/dsl/helpers.rb +8 -3
  17. data/lib/grape/dsl/inside_route.rb +67 -44
  18. data/lib/grape/dsl/parameters.rb +21 -12
  19. data/lib/grape/dsl/request_response.rb +1 -1
  20. data/lib/grape/dsl/routing.rb +3 -4
  21. data/lib/grape/endpoint.rb +63 -28
  22. data/lib/grape/error_formatter/base.rb +6 -6
  23. data/lib/grape/exceptions/base.rb +5 -5
  24. data/lib/grape/exceptions/invalid_version_header.rb +10 -0
  25. data/lib/grape/formatter/serializable_hash.rb +3 -2
  26. data/lib/grape/locale/en.yml +4 -1
  27. data/lib/grape/middleware/auth/base.rb +2 -2
  28. data/lib/grape/middleware/auth/dsl.rb +1 -1
  29. data/lib/grape/middleware/auth/strategies.rb +1 -1
  30. data/lib/grape/middleware/base.rb +7 -4
  31. data/lib/grape/middleware/error.rb +3 -2
  32. data/lib/grape/middleware/filter.rb +1 -1
  33. data/lib/grape/middleware/formatter.rb +47 -44
  34. data/lib/grape/middleware/globals.rb +3 -3
  35. data/lib/grape/middleware/versioner/accept_version_header.rb +5 -7
  36. data/lib/grape/middleware/versioner/header.rb +113 -50
  37. data/lib/grape/middleware/versioner/param.rb +5 -8
  38. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +20 -0
  39. data/lib/grape/middleware/versioner/path.rb +3 -6
  40. data/lib/grape/path.rb +3 -3
  41. data/lib/grape/request.rb +40 -0
  42. data/lib/grape/util/content_types.rb +9 -9
  43. data/lib/grape/util/env.rb +22 -0
  44. data/lib/grape/util/strict_hash_configuration.rb +2 -1
  45. data/lib/grape/validations/attributes_iterator.rb +8 -3
  46. data/lib/grape/validations/params_scope.rb +83 -15
  47. data/lib/grape/validations/types.rb +144 -0
  48. data/lib/grape/validations/types/build_coercer.rb +53 -0
  49. data/lib/grape/validations/types/custom_type_coercer.rb +183 -0
  50. data/lib/grape/validations/types/file.rb +28 -0
  51. data/lib/grape/validations/types/json.rb +65 -0
  52. data/lib/grape/validations/types/multiple_type_coercer.rb +76 -0
  53. data/lib/grape/validations/types/variant_collection_coercer.rb +59 -0
  54. data/lib/grape/validations/types/virtus_collection_patch.rb +16 -0
  55. data/lib/grape/validations/validators/all_or_none.rb +1 -1
  56. data/lib/grape/validations/validators/allow_blank.rb +3 -3
  57. data/lib/grape/validations/validators/base.rb +7 -0
  58. data/lib/grape/validations/validators/coerce.rb +31 -42
  59. data/lib/grape/validations/validators/presence.rb +2 -3
  60. data/lib/grape/validations/validators/regexp.rb +2 -4
  61. data/lib/grape/validations/validators/values.rb +3 -3
  62. data/lib/grape/version.rb +1 -1
  63. data/pkg/grape-0.13.0.gem +0 -0
  64. data/spec/grape/api/custom_validations_spec.rb +5 -4
  65. data/spec/grape/api/deeply_included_options_spec.rb +7 -7
  66. data/spec/grape/api/nested_helpers_spec.rb +4 -2
  67. data/spec/grape/api/shared_helpers_spec.rb +8 -8
  68. data/spec/grape/api_spec.rb +88 -54
  69. data/spec/grape/dsl/configuration_spec.rb +13 -0
  70. data/spec/grape/dsl/helpers_spec.rb +16 -2
  71. data/spec/grape/dsl/inside_route_spec.rb +3 -2
  72. data/spec/grape/dsl/parameters_spec.rb +0 -6
  73. data/spec/grape/dsl/routing_spec.rb +1 -1
  74. data/spec/grape/endpoint_spec.rb +61 -20
  75. data/spec/grape/entity_spec.rb +10 -8
  76. data/spec/grape/exceptions/invalid_accept_header_spec.rb +1 -15
  77. data/spec/grape/integration/rack_spec.rb +3 -2
  78. data/spec/grape/middleware/base_spec.rb +7 -5
  79. data/spec/grape/middleware/error_spec.rb +16 -15
  80. data/spec/grape/middleware/exception_spec.rb +45 -43
  81. data/spec/grape/middleware/formatter_spec.rb +34 -0
  82. data/spec/grape/middleware/versioner/header_spec.rb +79 -47
  83. data/spec/grape/path_spec.rb +10 -10
  84. data/spec/grape/presenters/presenter_spec.rb +2 -2
  85. data/spec/grape/request_spec.rb +100 -0
  86. data/spec/grape/validations/params_scope_spec.rb +11 -9
  87. data/spec/grape/validations/types_spec.rb +95 -0
  88. data/spec/grape/validations/validators/coerce_spec.rb +335 -2
  89. data/spec/grape/validations/validators/values_spec.rb +15 -15
  90. data/spec/grape/validations_spec.rb +53 -24
  91. data/spec/shared/versioning_examples.rb +2 -2
  92. data/spec/spec_helper.rb +0 -1
  93. data/spec/support/versioned_helpers.rb +2 -2
  94. metadata +51 -13
  95. data/.gitignore +0 -46
  96. data/.rspec +0 -2
  97. data/.rubocop.yml +0 -7
  98. data/.rubocop_todo.yml +0 -84
  99. data/.travis.yml +0 -20
  100. data/.yardopts +0 -2
  101. data/lib/grape/http/request.rb +0 -35
  102. data/lib/grape/util/parameter_types.rb +0 -58
  103. data/spec/grape/util/parameter_types_spec.rb +0 -54
@@ -82,47 +82,47 @@ module Grape
82
82
  end
83
83
  end
84
84
 
85
- describe '#has_namespace?' do
85
+ describe '#namespace?' do
86
86
  it 'is false when the namespace is nil' do
87
87
  path = Path.new(anything, nil, anything)
88
- expect(path).not_to have_namespace
88
+ expect(path.namespace?).to be nil
89
89
  end
90
90
 
91
91
  it 'is false when the namespace starts with whitespace' do
92
92
  path = Path.new(anything, ' /foo', anything)
93
- expect(path).not_to have_namespace
93
+ expect(path.namespace?).to be nil
94
94
  end
95
95
 
96
96
  it 'is false when the namespace is the root path' do
97
97
  path = Path.new(anything, '/', anything)
98
- expect(path).not_to have_namespace
98
+ expect(path.namespace?).to be false
99
99
  end
100
100
 
101
101
  it 'is true otherwise' do
102
102
  path = Path.new(anything, '/world', anything)
103
- expect(path).to have_namespace
103
+ expect(path.namespace?).to be true
104
104
  end
105
105
  end
106
106
 
107
- describe '#has_path?' do
107
+ describe '#path?' do
108
108
  it 'is false when the path is nil' do
109
109
  path = Path.new(nil, anything, anything)
110
- expect(path).not_to have_path
110
+ expect(path.path?).to be nil
111
111
  end
112
112
 
113
113
  it 'is false when the path starts with whitespace' do
114
114
  path = Path.new(' /foo', anything, anything)
115
- expect(path).not_to have_path
115
+ expect(path.path?).to be nil
116
116
  end
117
117
 
118
118
  it 'is false when the path is the root path' do
119
119
  path = Path.new('/', anything, anything)
120
- expect(path).not_to have_path
120
+ expect(path.path?).to be false
121
121
  end
122
122
 
123
123
  it 'is true otherwise' do
124
124
  path = Path.new('/hello', anything, anything)
125
- expect(path).to have_path
125
+ expect(path.path?).to be true
126
126
  end
127
127
  end
128
128
 
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  module Grape
4
4
  module Presenters
5
- module InsideRouteSpec
5
+ module PresenterSpec
6
6
  class Dummy
7
7
  include Grape::DSL::InsideRoute
8
8
 
@@ -27,7 +27,7 @@ module Grape
27
27
  end
28
28
  end
29
29
 
30
- subject { InsideRouteSpec::Dummy.new }
30
+ subject { PresenterSpec::Dummy.new }
31
31
 
32
32
  describe 'present' do
33
33
  let(:hash_mock) do
@@ -0,0 +1,100 @@
1
+ require 'spec_helper'
2
+
3
+ module Grape
4
+ describe Request do
5
+ let(:default_method) { 'GET' }
6
+ let(:default_params) { {} }
7
+ let(:default_options) {
8
+ {
9
+ method: method,
10
+ params: params
11
+ }
12
+ }
13
+ let(:default_env) {
14
+ Rack::MockRequest.env_for('/', options)
15
+ }
16
+ let(:method) { default_method }
17
+ let(:params) { default_params }
18
+ let(:options) { default_options }
19
+ let(:env) { default_env }
20
+
21
+ let(:request) {
22
+ Grape::Request.new(env)
23
+ }
24
+
25
+ describe '#params' do
26
+ let(:params) {
27
+ {
28
+ a: '123',
29
+ b: 'xyz'
30
+ }
31
+ }
32
+
33
+ it 'returns params' do
34
+ expect(request.params).to eq('a' => '123', 'b' => 'xyz')
35
+ end
36
+
37
+ describe 'with rack.routing_args' do
38
+ let(:options) {
39
+ default_options.merge('rack.routing_args' => routing_args)
40
+ }
41
+ let(:routing_args) {
42
+ {
43
+ version: '123',
44
+ route_info: '456',
45
+ c: 'ccc'
46
+ }
47
+ }
48
+
49
+ it 'cuts version and route_info' do
50
+ expect(request.params).to eq('a' => '123', 'b' => 'xyz', 'c' => 'ccc')
51
+ end
52
+ end
53
+ end
54
+
55
+ describe '#headers' do
56
+ let(:options) {
57
+ default_options.merge(request_headers)
58
+ }
59
+
60
+ describe 'with http headers in env' do
61
+ let(:request_headers) {
62
+ {
63
+ 'HTTP_X_GRAPE_IS_COOL' => 'yeah'
64
+ }
65
+ }
66
+
67
+ it 'cuts HTTP_ prefix and capitalizes header name words' do
68
+ expect(request.headers).to eq('X-Grape-Is-Cool' => 'yeah')
69
+ end
70
+ end
71
+
72
+ describe 'with non-HTTP_* stuff in env' do
73
+ let(:request_headers) {
74
+ {
75
+ 'HTP_X_GRAPE_ENTITY_TOO' => 'but now we are testing Grape'
76
+ }
77
+ }
78
+
79
+ it 'does not include them' do
80
+ expect(request.headers).to eq({})
81
+ end
82
+ end
83
+
84
+ describe 'with symbolic header names' do
85
+ let(:request_headers) {
86
+ {
87
+ HTTP_GRAPE_LIKES_SYMBOLIC: 'it is true'
88
+ }
89
+ }
90
+ let(:env) {
91
+ default_env.merge(request_headers)
92
+ }
93
+
94
+ it 'converts them to string' do
95
+ expect(request.headers).to eq('Grape-Likes-Symbolic' => 'it is true')
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -90,21 +90,23 @@ describe Grape::Validations::ParamsScope do
90
90
  end
91
91
 
92
92
  context 'when using custom types' do
93
- class CustomType
94
- attr_reader :value
95
- def self.parse(value)
96
- fail if value == 'invalid'
97
- new(value)
98
- end
93
+ module ParamsScopeSpec
94
+ class CustomType
95
+ attr_reader :value
96
+ def self.parse(value)
97
+ fail if value == 'invalid'
98
+ new(value)
99
+ end
99
100
 
100
- def initialize(value)
101
- @value = value
101
+ def initialize(value)
102
+ @value = value
103
+ end
102
104
  end
103
105
  end
104
106
 
105
107
  it 'coerces the parameter via the type\'s parse method' do
106
108
  subject.params do
107
- requires :foo, type: CustomType
109
+ requires :foo, type: ParamsScopeSpec::CustomType
108
110
  end
109
111
  subject.get('/types') { params[:foo].value }
110
112
 
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+
3
+ describe Grape::Validations::Types do
4
+ module TypesSpec
5
+ class FooType
6
+ def self.parse(_)
7
+ end
8
+ end
9
+
10
+ class BarType
11
+ def self.parse
12
+ end
13
+ end
14
+ end
15
+
16
+ VirtusA = Virtus::Attribute.build(String)
17
+
18
+ module VirtusModule
19
+ include Virtus.module
20
+ end
21
+
22
+ class VirtusB
23
+ include VirtusModule
24
+ end
25
+
26
+ class VirtusC
27
+ include Virtus.model
28
+ end
29
+
30
+ MyAxiom = Axiom::Types::String.new do
31
+ minimum_length 1
32
+ maximum_length 30
33
+ end
34
+
35
+ describe '::primitive?' do
36
+ [
37
+ Integer, Float, Numeric, BigDecimal,
38
+ Virtus::Attribute::Boolean, String, Symbol,
39
+ Date, DateTime, Time, Rack::Multipart::UploadedFile
40
+ ].each do |type|
41
+ it "recognizes #{type} as a primitive" do
42
+ expect(described_class.primitive?(type)).to be_truthy
43
+ end
44
+ end
45
+
46
+ it 'identifies unknown types' do
47
+ expect(described_class.primitive?(Object)).to be_falsy
48
+ expect(described_class.primitive?(TypesSpec::FooType)).to be_falsy
49
+ end
50
+ end
51
+
52
+ describe '::structure?' do
53
+ [
54
+ Hash, Array, Set
55
+ ].each do |type|
56
+ it "recognizes #{type} as a structure" do
57
+ expect(described_class.structure?(type)).to be_truthy
58
+ end
59
+ end
60
+ end
61
+
62
+ describe '::recognized?' do
63
+ [
64
+ VirtusA, VirtusB, VirtusC, MyAxiom
65
+ ].each do |type|
66
+ it "recognizes #{type}" do
67
+ expect(described_class.recognized?(type)).to be_truthy
68
+ end
69
+ end
70
+ end
71
+
72
+ describe '::special?' do
73
+ [
74
+ JSON, Array[JSON], File, Rack::Multipart::UploadedFile
75
+ ].each do |type|
76
+ it "provides special handling for #{type.inspect}" do
77
+ expect(described_class.special?(type)).to be_truthy
78
+ end
79
+ end
80
+ end
81
+
82
+ describe '::custom?' do
83
+ it 'returns false if the type does not respond to :parse' do
84
+ expect(described_class.custom?(Object)).to be_falsy
85
+ end
86
+
87
+ it 'returns true if the type responds to :parse with one argument' do
88
+ expect(described_class.custom?(TypesSpec::FooType)).to be_truthy
89
+ end
90
+
91
+ it 'returns false if the type\'s #parse method takes other than one argument' do
92
+ expect(described_class.custom?(TypesSpec::BarType)).to be_falsy
93
+ end
94
+ end
95
+ end
@@ -224,9 +224,9 @@ describe Grape::Validations::CoerceValidator do
224
224
  expect(last_response.body).to eq('TrueClass')
225
225
  end
226
226
 
227
- it 'file' do
227
+ it 'Rack::Multipart::UploadedFile' do
228
228
  subject.params do
229
- requires :file, coerce: Rack::Multipart::UploadedFile
229
+ requires :file, type: Rack::Multipart::UploadedFile
230
230
  end
231
231
  subject.post '/upload' do
232
232
  params[:file].filename
@@ -235,6 +235,27 @@ describe Grape::Validations::CoerceValidator do
235
235
  post '/upload', file: Rack::Test::UploadedFile.new(__FILE__)
236
236
  expect(last_response.status).to eq(201)
237
237
  expect(last_response.body).to eq(File.basename(__FILE__).to_s)
238
+
239
+ post '/upload', file: 'not a file'
240
+ expect(last_response.status).to eq(400)
241
+ expect(last_response.body).to eq('file is invalid')
242
+ end
243
+
244
+ it 'File' do
245
+ subject.params do
246
+ requires :file, coerce: File
247
+ end
248
+ subject.post '/upload' do
249
+ params[:file].filename
250
+ end
251
+
252
+ post '/upload', file: Rack::Test::UploadedFile.new(__FILE__)
253
+ expect(last_response.status).to eq(201)
254
+ expect(last_response.body).to eq(File.basename(__FILE__).to_s)
255
+
256
+ post '/upload', file: 'not a file'
257
+ expect(last_response.status).to eq(400)
258
+ expect(last_response.body).to eq('file is invalid')
238
259
  end
239
260
 
240
261
  it 'Nests integers' do
@@ -252,5 +273,317 @@ describe Grape::Validations::CoerceValidator do
252
273
  expect(last_response.body).to eq('Fixnum')
253
274
  end
254
275
  end
276
+
277
+ context 'using coerce_with' do
278
+ it 'uses parse where available' do
279
+ subject.params do
280
+ requires :ints, type: Array, coerce_with: JSON do
281
+ requires :i, type: Integer
282
+ requires :j
283
+ end
284
+ end
285
+ subject.get '/ints' do
286
+ ints = params[:ints].first
287
+ 'coercion works' if ints[:i] == 1 && ints[:j] == '2'
288
+ end
289
+
290
+ get '/ints', ints: [{ i: 1, j: '2' }]
291
+ expect(last_response.status).to eq(400)
292
+ expect(last_response.body).to eq('ints is invalid')
293
+
294
+ get '/ints', ints: '{"i":1,"j":"2"}'
295
+ expect(last_response.status).to eq(400)
296
+ expect(last_response.body).to eq('ints[0][i] is missing, ints[0][i] is invalid, ints[0][j] is missing')
297
+
298
+ get '/ints', ints: '[{"i":"1","j":"2"}]'
299
+ expect(last_response.status).to eq(200)
300
+ expect(last_response.body).to eq('coercion works')
301
+ end
302
+
303
+ it 'accepts any callable' do
304
+ subject.params do
305
+ requires :ints, type: Hash, coerce_with: JSON.method(:parse) do
306
+ requires :int, type: Integer, coerce_with: ->(val) { val == 'three' ? 3 : val }
307
+ end
308
+ end
309
+ subject.get '/ints' do
310
+ params[:ints][:int]
311
+ end
312
+
313
+ get '/ints', ints: '{"int":"3"}'
314
+ expect(last_response.status).to eq(400)
315
+ expect(last_response.body).to eq('ints[int] is invalid')
316
+
317
+ get '/ints', ints: '{"int":"three"}'
318
+ expect(last_response.status).to eq(200)
319
+ expect(last_response.body).to eq('3')
320
+
321
+ get '/ints', ints: '{"int":3}'
322
+ expect(last_response.status).to eq(200)
323
+ expect(last_response.body).to eq('3')
324
+ end
325
+
326
+ it 'must be supplied with :type or :coerce' do
327
+ expect do
328
+ subject.params do
329
+ requires :ints, coerce_with: JSON
330
+ end
331
+ end.to raise_error(ArgumentError)
332
+ end
333
+ end
334
+
335
+ context 'first-class JSON' do
336
+ it 'parses objects and arrays' do
337
+ subject.params do
338
+ requires :splines, type: JSON do
339
+ requires :x, type: Integer, values: [1, 2, 3]
340
+ optional :ints, type: Array[Integer]
341
+ optional :obj, type: Hash do
342
+ optional :y
343
+ end
344
+ end
345
+ end
346
+ subject.get '/' do
347
+ if params[:splines].is_a? Hash
348
+ params[:splines][:obj][:y]
349
+ else
350
+ 'arrays work' if params[:splines].any? { |s| s.key? :obj }
351
+ end
352
+ end
353
+
354
+ get '/', splines: '{"x":1,"ints":[1,2,3],"obj":{"y":"woof"}}'
355
+ expect(last_response.status).to eq(200)
356
+ expect(last_response.body).to eq('woof')
357
+
358
+ get '/', splines: '[{"x":2,"ints":[]},{"x":3,"ints":[4],"obj":{"y":"quack"}}]'
359
+ expect(last_response.status).to eq(200)
360
+ expect(last_response.body).to eq('arrays work')
361
+
362
+ get '/', splines: '{"x":4,"ints":[2]}'
363
+ expect(last_response.status).to eq(400)
364
+ expect(last_response.body).to eq('splines[x] does not have a valid value')
365
+
366
+ get '/', splines: '[{"x":1,"ints":[]},{"x":4,"ints":[]}]'
367
+ expect(last_response.status).to eq(400)
368
+ expect(last_response.body).to eq('splines[x] does not have a valid value')
369
+ end
370
+
371
+ it 'accepts Array[JSON] shorthand' do
372
+ subject.params do
373
+ requires :splines, type: Array[JSON] do
374
+ requires :x, type: Integer, values: [1, 2, 3]
375
+ requires :y
376
+ end
377
+ end
378
+ subject.get '/' do
379
+ params[:splines].first[:y].class.to_s
380
+ spline = params[:splines].first
381
+ "#{spline[:x].class}.#{spline[:y].class}"
382
+ end
383
+
384
+ get '/', splines: '{"x":"1","y":"woof"}'
385
+ expect(last_response.status).to eq(200)
386
+ expect(last_response.body).to eq('Fixnum.String')
387
+
388
+ get '/', splines: '[{"x":1,"y":2},{"x":1,"y":"quack"}]'
389
+ expect(last_response.status).to eq(200)
390
+ expect(last_response.body).to eq('Fixnum.Fixnum')
391
+
392
+ get '/', splines: '{"x":"4","y":"woof"}'
393
+ expect(last_response.status).to eq(400)
394
+ expect(last_response.body).to eq('splines[x] does not have a valid value')
395
+
396
+ get '/', splines: '[{"x":"4","y":"woof"}]'
397
+ expect(last_response.status).to eq(400)
398
+ expect(last_response.body).to eq('splines[x] does not have a valid value')
399
+ end
400
+
401
+ it "doesn't make sense using coerce_with" do
402
+ expect do
403
+ subject.params do
404
+ requires :bad, type: JSON, coerce_with: JSON do
405
+ requires :x
406
+ end
407
+ end
408
+ end.to raise_error(ArgumentError)
409
+
410
+ expect do
411
+ subject.params do
412
+ requires :bad, type: Array[JSON], coerce_with: JSON do
413
+ requires :x
414
+ end
415
+ end
416
+ end.to raise_error(ArgumentError)
417
+ end
418
+ end
419
+
420
+ context 'multiple types' do
421
+ Boolean = Grape::API::Boolean
422
+
423
+ it 'coerces to first possible type' do
424
+ subject.params do
425
+ requires :a, types: [Boolean, Integer, String]
426
+ end
427
+ subject.get '/' do
428
+ params[:a].class.to_s
429
+ end
430
+
431
+ get '/', a: 'true'
432
+ expect(last_response.status).to eq(200)
433
+ expect(last_response.body).to eq('TrueClass')
434
+
435
+ get '/', a: '5'
436
+ expect(last_response.status).to eq(200)
437
+ expect(last_response.body).to eq('Fixnum')
438
+
439
+ get '/', a: 'anything else'
440
+ expect(last_response.status).to eq(200)
441
+ expect(last_response.body).to eq('String')
442
+ end
443
+
444
+ it 'fails when no coercion is possible' do
445
+ subject.params do
446
+ requires :a, types: [Boolean, Integer]
447
+ end
448
+ subject.get '/' do
449
+ params[:a].class.to_s
450
+ end
451
+
452
+ get '/', a: true
453
+ expect(last_response.status).to eq(200)
454
+ expect(last_response.body).to eq('TrueClass')
455
+
456
+ get '/', a: 'not good'
457
+ expect(last_response.status).to eq(400)
458
+ expect(last_response.body).to eq('a is invalid')
459
+ end
460
+
461
+ context 'for primitive collections' do
462
+ before do
463
+ subject.params do
464
+ optional :a, types: [String, Array[String]]
465
+ optional :b, types: [Array[Integer], Array[String]]
466
+ optional :c, type: Array[Integer, String]
467
+ optional :d, types: [Integer, String, Set[Integer, String]]
468
+ end
469
+ subject.get '/' do
470
+ (
471
+ params[:a] ||
472
+ params[:b] ||
473
+ params[:c] ||
474
+ params[:d]
475
+ ).inspect
476
+ end
477
+ end
478
+
479
+ it 'allows singular form declaration' do
480
+ get '/', a: 'one way'
481
+ expect(last_response.status).to eq(200)
482
+ expect(last_response.body).to eq('"one way"')
483
+
484
+ get '/', a: %w(the other)
485
+ expect(last_response.status).to eq(200)
486
+ expect(last_response.body).to eq('["the", "other"]')
487
+
488
+ get '/', a: { a: 1, b: 2 }
489
+ expect(last_response.status).to eq(400)
490
+ expect(last_response.body).to eq('a is invalid')
491
+
492
+ get '/', a: [1, 2, 3]
493
+ expect(last_response.status).to eq(200)
494
+ expect(last_response.body).to eq('["1", "2", "3"]')
495
+ end
496
+
497
+ it 'allows multiple collection types' do
498
+ get '/', b: [1, 2, 3]
499
+ expect(last_response.status).to eq(200)
500
+ expect(last_response.body).to eq('[1, 2, 3]')
501
+
502
+ get '/', b: %w(1 2 3)
503
+ expect(last_response.status).to eq(200)
504
+ expect(last_response.body).to eq('[1, 2, 3]')
505
+
506
+ get '/', b: [1, true, 'three']
507
+ expect(last_response.status).to eq(200)
508
+ expect(last_response.body).to eq('["1", "true", "three"]')
509
+ end
510
+
511
+ it 'allows collections with multiple types' do
512
+ get '/', c: [1, '2', true, 'three']
513
+ expect(last_response.status).to eq(200)
514
+ expect(last_response.body).to eq('[1, 2, "true", "three"]')
515
+
516
+ get '/', d: '1'
517
+ expect(last_response.status).to eq(200)
518
+ expect(last_response.body).to eq('1')
519
+
520
+ get '/', d: 'one'
521
+ expect(last_response.status).to eq(200)
522
+ expect(last_response.body).to eq('"one"')
523
+
524
+ get '/', d: %w(1 two)
525
+ expect(last_response.status).to eq(200)
526
+ expect(last_response.body).to eq('#<Set: {1, "two"}>')
527
+ end
528
+ end
529
+
530
+ context 'custom coercion rules' do
531
+ before do
532
+ subject.params do
533
+ requires :a, types: [Boolean, String], coerce_with: (lambda do |val|
534
+ if val == 'yup'
535
+ true
536
+ elsif val == 'false'
537
+ 0
538
+ else
539
+ val
540
+ end
541
+ end)
542
+ end
543
+ subject.get '/' do
544
+ params[:a].class.to_s
545
+ end
546
+ end
547
+
548
+ it 'respects :coerce_with' do
549
+ get '/', a: 'yup'
550
+ expect(last_response.status).to eq(200)
551
+ expect(last_response.body).to eq('TrueClass')
552
+ end
553
+
554
+ it 'still validates type' do
555
+ get '/', a: 'false'
556
+ expect(last_response.status).to eq(400)
557
+ expect(last_response.body).to eq('a is invalid')
558
+ end
559
+
560
+ it 'performs no additional coercion' do
561
+ get '/', a: 'true'
562
+ expect(last_response.status).to eq(200)
563
+ expect(last_response.body).to eq('String')
564
+ end
565
+ end
566
+
567
+ it 'may not be supplied together with a single type' do
568
+ expect do
569
+ subject.params do
570
+ requires :a, type: Integer, types: [Integer, String]
571
+ end
572
+ end.to raise_exception ArgumentError
573
+ end
574
+ end
575
+
576
+ context 'converter' do
577
+ it 'does not build Virtus::Attribute multiple times' do
578
+ subject.params do
579
+ requires :something, type: Array[String]
580
+ end
581
+ subject.get do
582
+ end
583
+
584
+ expect(Virtus::Attribute).to receive(:build).at_most(2).times.and_call_original
585
+ 10.times { get '/' }
586
+ end
587
+ end
255
588
  end
256
589
  end