grape 1.3.2 → 1.5.2

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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -2
  3. data/LICENSE +1 -1
  4. data/README.md +120 -24
  5. data/UPGRADING.md +220 -39
  6. data/lib/grape.rb +3 -2
  7. data/lib/grape/api.rb +3 -3
  8. data/lib/grape/api/instance.rb +22 -25
  9. data/lib/grape/dsl/callbacks.rb +1 -1
  10. data/lib/grape/dsl/helpers.rb +1 -0
  11. data/lib/grape/dsl/inside_route.rb +70 -37
  12. data/lib/grape/dsl/parameters.rb +8 -4
  13. data/lib/grape/dsl/routing.rb +6 -7
  14. data/lib/grape/dsl/validations.rb +18 -1
  15. data/lib/grape/eager_load.rb +1 -1
  16. data/lib/grape/endpoint.rb +8 -6
  17. data/lib/grape/exceptions/validation.rb +1 -1
  18. data/lib/grape/exceptions/validation_errors.rb +1 -1
  19. data/lib/grape/middleware/auth/base.rb +3 -3
  20. data/lib/grape/middleware/base.rb +3 -2
  21. data/lib/grape/middleware/error.rb +11 -13
  22. data/lib/grape/middleware/formatter.rb +3 -3
  23. data/lib/grape/middleware/stack.rb +8 -1
  24. data/lib/grape/request.rb +1 -1
  25. data/lib/grape/router.rb +25 -39
  26. data/lib/grape/router/attribute_translator.rb +26 -5
  27. data/lib/grape/router/route.rb +1 -19
  28. data/lib/grape/{serve_file → serve_stream}/file_body.rb +1 -1
  29. data/lib/grape/{serve_file → serve_stream}/sendfile_response.rb +1 -1
  30. data/lib/grape/{serve_file/file_response.rb → serve_stream/stream_response.rb} +8 -8
  31. data/lib/grape/util/base_inheritable.rb +2 -2
  32. data/lib/grape/util/lazy_value.rb +1 -0
  33. data/lib/grape/validations/attributes_iterator.rb +8 -0
  34. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  35. data/lib/grape/validations/params_scope.rb +9 -7
  36. data/lib/grape/validations/single_attribute_iterator.rb +1 -1
  37. data/lib/grape/validations/types.rb +1 -4
  38. data/lib/grape/validations/types/array_coercer.rb +14 -5
  39. data/lib/grape/validations/types/build_coercer.rb +1 -5
  40. data/lib/grape/validations/types/custom_type_coercer.rb +15 -1
  41. data/lib/grape/validations/types/dry_type_coercer.rb +36 -1
  42. data/lib/grape/validations/types/invalid_value.rb +24 -0
  43. data/lib/grape/validations/types/primitive_coercer.rb +9 -3
  44. data/lib/grape/validations/types/set_coercer.rb +6 -4
  45. data/lib/grape/validations/types/variant_collection_coercer.rb +1 -1
  46. data/lib/grape/validations/validator_factory.rb +1 -1
  47. data/lib/grape/validations/validators/as.rb +1 -1
  48. data/lib/grape/validations/validators/base.rb +8 -8
  49. data/lib/grape/validations/validators/coerce.rb +8 -14
  50. data/lib/grape/validations/validators/default.rb +3 -5
  51. data/lib/grape/validations/validators/except_values.rb +1 -1
  52. data/lib/grape/validations/validators/multiple_params_base.rb +2 -1
  53. data/lib/grape/validations/validators/values.rb +1 -1
  54. data/lib/grape/version.rb +1 -1
  55. data/spec/grape/api/instance_spec.rb +50 -0
  56. data/spec/grape/api_remount_spec.rb +9 -4
  57. data/spec/grape/api_spec.rb +75 -0
  58. data/spec/grape/dsl/inside_route_spec.rb +182 -33
  59. data/spec/grape/endpoint/declared_spec.rb +601 -0
  60. data/spec/grape/endpoint_spec.rb +0 -521
  61. data/spec/grape/entity_spec.rb +7 -1
  62. data/spec/grape/integration/rack_sendfile_spec.rb +12 -8
  63. data/spec/grape/middleware/auth/strategies_spec.rb +1 -1
  64. data/spec/grape/middleware/error_spec.rb +1 -1
  65. data/spec/grape/middleware/formatter_spec.rb +1 -1
  66. data/spec/grape/middleware/stack_spec.rb +1 -0
  67. data/spec/grape/request_spec.rb +1 -1
  68. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +13 -3
  69. data/spec/grape/validations/params_scope_spec.rb +26 -0
  70. data/spec/grape/validations/single_attribute_iterator_spec.rb +17 -6
  71. data/spec/grape/validations/types/array_coercer_spec.rb +35 -0
  72. data/spec/grape/validations/types/primitive_coercer_spec.rb +65 -5
  73. data/spec/grape/validations/types/set_coercer_spec.rb +34 -0
  74. data/spec/grape/validations/validators/coerce_spec.rb +223 -25
  75. data/spec/grape/validations/validators/default_spec.rb +170 -0
  76. data/spec/grape/validations/validators/except_values_spec.rb +1 -0
  77. data/spec/grape/validations/validators/values_spec.rb +1 -1
  78. data/spec/grape/validations_spec.rb +290 -18
  79. data/spec/integration/eager_load/eager_load_spec.rb +15 -0
  80. data/spec/shared/versioning_examples.rb +20 -20
  81. data/spec/spec_helper.rb +0 -10
  82. data/spec/support/chunks.rb +14 -0
  83. data/spec/support/versioned_helpers.rb +4 -6
  84. metadata +20 -9
@@ -116,7 +116,7 @@ describe Grape::Entity do
116
116
  expect(last_response.body).to eq('Auto-detect!')
117
117
  end
118
118
 
119
- it 'does not run autodetection for Entity when explicitely provided' do
119
+ it 'does not run autodetection for Entity when explicitly provided' do
120
120
  entity = Class.new(Grape::Entity)
121
121
  some_array = []
122
122
 
@@ -181,6 +181,7 @@ describe Grape::Entity do
181
181
  subject.get '/example' do
182
182
  c = Class.new do
183
183
  attr_reader :id
184
+
184
185
  def initialize(id)
185
186
  @id = id
186
187
  end
@@ -202,6 +203,7 @@ describe Grape::Entity do
202
203
  subject.get '/examples' do
203
204
  c = Class.new do
204
205
  attr_reader :id
206
+
205
207
  def initialize(id)
206
208
  @id = id
207
209
  end
@@ -226,6 +228,7 @@ describe Grape::Entity do
226
228
  subject.get '/example' do
227
229
  c = Class.new do
228
230
  attr_reader :name
231
+
229
232
  def initialize(args)
230
233
  @name = args[:name] || 'no name set'
231
234
  end
@@ -255,6 +258,7 @@ XML
255
258
  subject.get '/example' do
256
259
  c = Class.new do
257
260
  attr_reader :name
261
+
258
262
  def initialize(args)
259
263
  @name = args[:name] || 'no name set'
260
264
  end
@@ -284,6 +288,7 @@ XML
284
288
  subject.get '/example' do
285
289
  c = Class.new do
286
290
  attr_reader :name
291
+
287
292
  def initialize(args)
288
293
  @name = args[:name] || 'no name set'
289
294
  end
@@ -302,6 +307,7 @@ XML
302
307
  it 'present with multiple entities using optional symbol' do
303
308
  user = Class.new do
304
309
  attr_reader :name
310
+
305
311
  def initialize(args)
306
312
  @name = args[:name] || 'no name set'
307
313
  end
@@ -4,12 +4,16 @@ require 'spec_helper'
4
4
 
5
5
  describe Rack::Sendfile do
6
6
  subject do
7
- send_file = file_streamer
7
+ content_object = file_object
8
8
  app = Class.new(Grape::API) do
9
9
  use Rack::Sendfile
10
10
  format :json
11
11
  get do
12
- file send_file
12
+ if content_object.is_a?(String)
13
+ sendfile content_object
14
+ else
15
+ stream content_object
16
+ end
13
17
  end
14
18
  end
15
19
 
@@ -22,9 +26,9 @@ describe Rack::Sendfile do
22
26
  app.call(env)
23
27
  end
24
28
 
25
- context do
26
- let(:file_streamer) do
27
- double(:file_streamer, to_path: '/accel/mapping/some/path')
29
+ context 'when calling sendfile' do
30
+ let(:file_object) do
31
+ '/accel/mapping/some/path'
28
32
  end
29
33
 
30
34
  it 'contains Sendfile headers' do
@@ -33,9 +37,9 @@ describe Rack::Sendfile do
33
37
  end
34
38
  end
35
39
 
36
- context do
37
- let(:file_streamer) do
38
- double(:file_streamer)
40
+ context 'when streaming non file content' do
41
+ let(:file_object) do
42
+ double(:file_object, each: nil)
39
43
  end
40
44
 
41
45
  it 'not contains Sendfile headers' do
@@ -36,7 +36,7 @@ describe Grape::Middleware::Auth::Strategies do
36
36
  RSpec::Matchers.define :be_challenge do
37
37
  match do |actual_response|
38
38
  actual_response.status == 401 &&
39
- actual_response['WWW-Authenticate'] =~ /^Digest / &&
39
+ actual_response['WWW-Authenticate'].start_with?('Digest ') &&
40
40
  actual_response.body.empty?
41
41
  end
42
42
  end
@@ -30,7 +30,7 @@ describe Grape::Middleware::Error do
30
30
  opts = options
31
31
  Rack::Builder.app do
32
32
  use Spec::Support::EndpointFaker
33
- use Grape::Middleware::Error, opts
33
+ use Grape::Middleware::Error, **opts
34
34
  run ErrorSpec::ErrApp
35
35
  end
36
36
  end
@@ -380,7 +380,7 @@ describe Grape::Middleware::Formatter do
380
380
 
381
381
  context 'send file' do
382
382
  let(:file) { double(File) }
383
- let(:file_body) { Grape::ServeFile::FileResponse.new(file) }
383
+ let(:file_body) { Grape::ServeStream::StreamResponse.new(file) }
384
384
  let(:app) { ->(_env) { [200, {}, file_body] } }
385
385
 
386
386
  it 'returns a file response' do
@@ -8,6 +8,7 @@ describe Grape::Middleware::Stack do
8
8
  class BarMiddleware; end
9
9
  class BlockMiddleware
10
10
  attr_reader :block
11
+
11
12
  def initialize(&block)
12
13
  @block = block
13
14
  end
@@ -75,7 +75,7 @@ module Grape
75
75
  Grape.config.reset
76
76
  end
77
77
 
78
- subject(:request_params) { Grape::Request.new(env, opts).params }
78
+ subject(:request_params) { Grape::Request.new(env, **opts).params }
79
79
 
80
80
  context 'when the API does not include a specific param builder' do
81
81
  let(:opts) { {} }
@@ -13,8 +13,8 @@ describe Grape::Validations::MultipleAttributesIterator do
13
13
  { first: 'string', second: 'string' }
14
14
  end
15
15
 
16
- it 'yields the whole params hash without the list of attrs' do
17
- expect { |b| iterator.each(&b) }.to yield_with_args(params)
16
+ it 'yields the whole params hash and the skipped flag without the list of attrs' do
17
+ expect { |b| iterator.each(&b) }.to yield_with_args(params, false)
18
18
  end
19
19
  end
20
20
 
@@ -24,7 +24,17 @@ describe Grape::Validations::MultipleAttributesIterator do
24
24
  end
25
25
 
26
26
  it 'yields each element of the array without the list of attrs' do
27
- expect { |b| iterator.each(&b) }.to yield_successive_args(params[0], params[1])
27
+ expect { |b| iterator.each(&b) }.to yield_successive_args([params[0], false], [params[1], false])
28
+ end
29
+ end
30
+
31
+ context 'when params is empty optional placeholder' do
32
+ let(:params) do
33
+ [Grape::DSL::Parameters::EmptyOptionalValue, { first: 'string2', second: 'string2' }]
34
+ end
35
+
36
+ it 'yields each element of the array without the list of attrs' do
37
+ expect { |b| iterator.each(&b) }.to yield_successive_args([Grape::DSL::Parameters::EmptyOptionalValue, true], [params[1], false])
28
38
  end
29
39
  end
30
40
  end
@@ -633,6 +633,32 @@ describe Grape::Validations::ParamsScope do
633
633
  expect(last_response.status).to eq(200)
634
634
  end
635
635
 
636
+ it 'detect unmet nested dependency' do
637
+ subject.params do
638
+ requires :a, type: String, allow_blank: false, values: %w[x y z]
639
+ given a: ->(val) { val == 'z' } do
640
+ requires :inner3, type: Array, allow_blank: false do
641
+ requires :bar, type: String, allow_blank: false
642
+ given bar: ->(val) { val == 'b' } do
643
+ requires :baz, type: Array do
644
+ optional :baz_category, type: String
645
+ end
646
+ end
647
+ given bar: ->(val) { val == 'c' } do
648
+ requires :baz, type: Array do
649
+ requires :baz_category, type: String
650
+ end
651
+ end
652
+ end
653
+ end
654
+ end
655
+ subject.get('/nested-dependency') { declared(params).to_json }
656
+
657
+ get '/nested-dependency', a: 'z', inner3: [{ bar: 'c', baz: [{ unrelated: 'nope' }] }]
658
+ expect(last_response.status).to eq(400)
659
+ expect(last_response.body).to eq 'inner3[0][baz][0][baz_category] is missing'
660
+ end
661
+
636
662
  it 'includes the parameter within #declared(params)' do
637
663
  get '/test', a: true, b: true
638
664
 
@@ -15,7 +15,7 @@ describe Grape::Validations::SingleAttributeIterator do
15
15
 
16
16
  it 'yields params and every single attribute from the list' do
17
17
  expect { |b| iterator.each(&b) }
18
- .to yield_successive_args([params, :first, false], [params, :second, false])
18
+ .to yield_successive_args([params, :first, false, false], [params, :second, false, false])
19
19
  end
20
20
  end
21
21
 
@@ -26,8 +26,8 @@ describe Grape::Validations::SingleAttributeIterator do
26
26
 
27
27
  it 'yields every single attribute from the list for each of the array elements' do
28
28
  expect { |b| iterator.each(&b) }.to yield_successive_args(
29
- [params[0], :first, false], [params[0], :second, false],
30
- [params[1], :first, false], [params[1], :second, false]
29
+ [params[0], :first, false, false], [params[0], :second, false, false],
30
+ [params[1], :first, false, false], [params[1], :second, false, false]
31
31
  )
32
32
  end
33
33
 
@@ -36,9 +36,20 @@ describe Grape::Validations::SingleAttributeIterator do
36
36
 
37
37
  it 'marks params with empty values' do
38
38
  expect { |b| iterator.each(&b) }.to yield_successive_args(
39
- [params[0], :first, true], [params[0], :second, true],
40
- [params[1], :first, true], [params[1], :second, true],
41
- [params[2], :first, false], [params[2], :second, false]
39
+ [params[0], :first, true, false], [params[0], :second, true, false],
40
+ [params[1], :first, true, false], [params[1], :second, true, false],
41
+ [params[2], :first, false, false], [params[2], :second, false, false]
42
+ )
43
+ end
44
+ end
45
+
46
+ context 'when missing optional value' do
47
+ let(:params) { [Grape::DSL::Parameters::EmptyOptionalValue, 10] }
48
+
49
+ it 'marks params with skipped values' do
50
+ expect { |b| iterator.each(&b) }.to yield_successive_args(
51
+ [params[0], :first, false, true], [params[0], :second, false, true],
52
+ [params[1], :first, false, false], [params[1], :second, false, false],
42
53
  )
43
54
  end
44
55
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Grape::Validations::Types::ArrayCoercer do
6
+ subject { described_class.new(type) }
7
+
8
+ describe '#call' do
9
+ context 'an array of primitives' do
10
+ let(:type) { Array[String] }
11
+
12
+ it 'coerces elements in the array' do
13
+ expect(subject.call([10, 20])).to eq(%w[10 20])
14
+ end
15
+ end
16
+
17
+ context 'an array of arrays' do
18
+ let(:type) { Array[Array[Integer]] }
19
+
20
+ it 'coerces elements in the nested array' do
21
+ expect(subject.call([%w[10 20]])).to eq([[10, 20]])
22
+ expect(subject.call([['10'], ['20']])).to eq([[10], [20]])
23
+ end
24
+ end
25
+
26
+ context 'an array of sets' do
27
+ let(:type) { Array[Set[Integer]] }
28
+
29
+ it 'coerces elements in the nested set' do
30
+ expect(subject.call([%w[10 20]])).to eq([Set[10, 20]])
31
+ expect(subject.call([['10'], ['20']])).to eq([Set[10], Set[20]])
32
+ end
33
+ end
34
+ end
35
+ end
@@ -7,7 +7,19 @@ describe Grape::Validations::Types::PrimitiveCoercer do
7
7
 
8
8
  subject { described_class.new(type, strict) }
9
9
 
10
- describe '.call' do
10
+ describe '#call' do
11
+ context 'BigDecimal' do
12
+ let(:type) { BigDecimal }
13
+
14
+ it 'coerces to BigDecimal' do
15
+ expect(subject.call(5)).to eq(BigDecimal(5))
16
+ end
17
+
18
+ it 'coerces an empty string to nil' do
19
+ expect(subject.call('')).to be_nil
20
+ end
21
+ end
22
+
11
23
  context 'Boolean' do
12
24
  let(:type) { Grape::API::Boolean }
13
25
 
@@ -26,6 +38,50 @@ describe Grape::Validations::Types::PrimitiveCoercer do
26
38
  it 'returns an error when the given value cannot be coerced' do
27
39
  expect(subject.call(123)).to be_instance_of(Grape::Validations::Types::InvalidValue)
28
40
  end
41
+
42
+ it 'coerces an empty string to nil' do
43
+ expect(subject.call('')).to be_nil
44
+ end
45
+ end
46
+
47
+ context 'DateTime' do
48
+ let(:type) { DateTime }
49
+
50
+ it 'coerces an empty string to nil' do
51
+ expect(subject.call('')).to be_nil
52
+ end
53
+ end
54
+
55
+ context 'Float' do
56
+ let(:type) { Float }
57
+
58
+ it 'coerces an empty string to nil' do
59
+ expect(subject.call('')).to be_nil
60
+ end
61
+ end
62
+
63
+ context 'Integer' do
64
+ let(:type) { Integer }
65
+
66
+ it 'coerces an empty string to nil' do
67
+ expect(subject.call('')).to be_nil
68
+ end
69
+ end
70
+
71
+ context 'Numeric' do
72
+ let(:type) { Numeric }
73
+
74
+ it 'coerces an empty string to nil' do
75
+ expect(subject.call('')).to be_nil
76
+ end
77
+ end
78
+
79
+ context 'Time' do
80
+ let(:type) { Time }
81
+
82
+ it 'coerces an empty string to nil' do
83
+ expect(subject.call('')).to be_nil
84
+ end
29
85
  end
30
86
 
31
87
  context 'String' do
@@ -34,13 +90,17 @@ describe Grape::Validations::Types::PrimitiveCoercer do
34
90
  it 'coerces to String' do
35
91
  expect(subject.call(10)).to eq('10')
36
92
  end
93
+
94
+ it 'does not coerce an empty string to nil' do
95
+ expect(subject.call('')).to eq('')
96
+ end
37
97
  end
38
98
 
39
- context 'BigDecimal' do
40
- let(:type) { BigDecimal }
99
+ context 'Symbol' do
100
+ let(:type) { Symbol }
41
101
 
42
- it 'coerces to BigDecimal' do
43
- expect(subject.call(5)).to eq(BigDecimal(5))
102
+ it 'coerces an empty string to nil' do
103
+ expect(subject.call('')).to be_nil
44
104
  end
45
105
  end
46
106
 
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Grape::Validations::Types::SetCoercer do
6
+ subject { described_class.new(type) }
7
+
8
+ describe '#call' do
9
+ context 'a set of primitives' do
10
+ let(:type) { Set[String] }
11
+
12
+ it 'coerces elements to the set' do
13
+ expect(subject.call([10, 20])).to eq(Set['10', '20'])
14
+ end
15
+ end
16
+
17
+ context 'a set of sets' do
18
+ let(:type) { Set[Set[Integer]] }
19
+
20
+ it 'coerces elements in the nested set' do
21
+ expect(subject.call([%w[10 20]])).to eq(Set[Set[10, 20]])
22
+ expect(subject.call([['10'], ['20']])).to eq(Set[Set[10], Set[20]])
23
+ end
24
+ end
25
+
26
+ context 'a set of sets of arrays' do
27
+ let(:type) { Set[Set[Array[Integer]]] }
28
+
29
+ it 'coerces elements in the nested set' do
30
+ expect(subject.call([[['10'], ['20']]])).to eq(Set[Set[Array[10], Array[20]]])
31
+ end
32
+ end
33
+ end
34
+ end
@@ -227,23 +227,51 @@ describe Grape::Validations::CoerceValidator do
227
227
  expect(last_response.body).to eq('NilClass')
228
228
  end
229
229
 
230
- it 'is a custom type' do
231
- subject.params do
232
- requires :uri, coerce: SecureURIOnly
233
- end
234
- subject.get '/secure_uri' do
235
- params[:uri].class
230
+ context 'a custom type' do
231
+ it 'coerces the given value' do
232
+ subject.params do
233
+ requires :uri, coerce: SecureURIOnly
234
+ end
235
+ subject.get '/secure_uri' do
236
+ params[:uri].class
237
+ end
238
+
239
+ get 'secure_uri', uri: 'https://www.example.com'
240
+
241
+ expect(last_response.status).to eq(200)
242
+ expect(last_response.body).to eq('URI::HTTPS')
243
+
244
+ get 'secure_uri', uri: 'http://www.example.com'
245
+
246
+ expect(last_response.status).to eq(400)
247
+ expect(last_response.body).to eq('uri is invalid')
236
248
  end
237
249
 
238
- get 'secure_uri', uri: 'https://www.example.com'
250
+ context 'returning the InvalidValue instance when invalid' do
251
+ let(:custom_type) do
252
+ Class.new do
253
+ def self.parse(_val)
254
+ Grape::Types::InvalidValue.new('must be unique')
255
+ end
256
+ end
257
+ end
239
258
 
240
- expect(last_response.status).to eq(200)
241
- expect(last_response.body).to eq('URI::HTTPS')
259
+ it 'uses a custom message added to the invalid value' do
260
+ type = custom_type
242
261
 
243
- get 'secure_uri', uri: 'http://www.example.com'
262
+ subject.params do
263
+ requires :name, type: type
264
+ end
265
+ subject.get '/whatever' do
266
+ params[:name].class
267
+ end
244
268
 
245
- expect(last_response.status).to eq(400)
246
- expect(last_response.body).to eq('uri is invalid')
269
+ get 'whatever', name: 'Bob'
270
+
271
+ expect(last_response.status).to eq(400)
272
+ expect(last_response.body).to eq('name must be unique')
273
+ end
274
+ end
247
275
  end
248
276
 
249
277
  context 'Array' do
@@ -424,6 +452,165 @@ describe Grape::Validations::CoerceValidator do
424
452
  expect(last_response.status).to eq(200)
425
453
  expect(last_response.body).to eq(integer_class_name)
426
454
  end
455
+
456
+ context 'nil values' do
457
+ context 'primitive types' do
458
+ Grape::Validations::Types::PRIMITIVES.each do |type|
459
+ it 'respects the nil value' do
460
+ subject.params do
461
+ requires :param, type: type
462
+ end
463
+ subject.get '/nil_value' do
464
+ params[:param].class
465
+ end
466
+
467
+ get '/nil_value', param: nil
468
+ expect(last_response.status).to eq(200)
469
+ expect(last_response.body).to eq('NilClass')
470
+ end
471
+ end
472
+ end
473
+
474
+ context 'structures types' do
475
+ Grape::Validations::Types::STRUCTURES.each do |type|
476
+ it 'respects the nil value' do
477
+ subject.params do
478
+ requires :param, type: type
479
+ end
480
+ subject.get '/nil_value' do
481
+ params[:param].class
482
+ end
483
+
484
+ get '/nil_value', param: nil
485
+ expect(last_response.status).to eq(200)
486
+ expect(last_response.body).to eq('NilClass')
487
+ end
488
+ end
489
+ end
490
+
491
+ context 'special types' do
492
+ Grape::Validations::Types::SPECIAL.each_key do |type|
493
+ it 'respects the nil value' do
494
+ subject.params do
495
+ requires :param, type: type
496
+ end
497
+ subject.get '/nil_value' do
498
+ params[:param].class
499
+ end
500
+
501
+ get '/nil_value', param: nil
502
+ expect(last_response.status).to eq(200)
503
+ expect(last_response.body).to eq('NilClass')
504
+ end
505
+ end
506
+
507
+ context 'variant-member-type collections' do
508
+ [
509
+ Array[Integer, String],
510
+ [Integer, String, Array[Integer, String]]
511
+ ].each do |type|
512
+ it 'respects the nil value' do
513
+ subject.params do
514
+ requires :param, type: type
515
+ end
516
+ subject.get '/nil_value' do
517
+ params[:param].class
518
+ end
519
+
520
+ get '/nil_value', param: nil
521
+ expect(last_response.status).to eq(200)
522
+ expect(last_response.body).to eq('NilClass')
523
+ end
524
+ end
525
+ end
526
+ end
527
+ end
528
+
529
+ context 'empty string' do
530
+ context 'primitive types' do
531
+ (Grape::Validations::Types::PRIMITIVES - [String]).each do |type|
532
+ it "is coerced to nil for type #{type}" do
533
+ subject.params do
534
+ requires :param, type: type
535
+ end
536
+ subject.get '/empty_string' do
537
+ params[:param].class
538
+ end
539
+
540
+ get '/empty_string', param: ''
541
+ expect(last_response.status).to eq(200)
542
+ expect(last_response.body).to eq('NilClass')
543
+ end
544
+ end
545
+
546
+ it 'is not coerced to nil for type String' do
547
+ subject.params do
548
+ requires :param, type: String
549
+ end
550
+ subject.get '/empty_string' do
551
+ params[:param].class
552
+ end
553
+
554
+ get '/empty_string', param: ''
555
+ expect(last_response.status).to eq(200)
556
+ expect(last_response.body).to eq('String')
557
+ end
558
+ end
559
+
560
+ context 'structures types' do
561
+ (Grape::Validations::Types::STRUCTURES - [Hash]).each do |type|
562
+ it "is coerced to nil for type #{type}" do
563
+ subject.params do
564
+ requires :param, type: type
565
+ end
566
+ subject.get '/empty_string' do
567
+ params[:param].class
568
+ end
569
+
570
+ get '/empty_string', param: ''
571
+ expect(last_response.status).to eq(200)
572
+ expect(last_response.body).to eq('NilClass')
573
+ end
574
+ end
575
+ end
576
+
577
+ context 'special types' do
578
+ (Grape::Validations::Types::SPECIAL.keys - [File, Rack::Multipart::UploadedFile]).each do |type|
579
+ it "is coerced to nil for type #{type}" do
580
+ subject.params do
581
+ requires :param, type: type
582
+ end
583
+ subject.get '/empty_string' do
584
+ params[:param].class
585
+ end
586
+
587
+ get '/empty_string', param: ''
588
+ expect(last_response.status).to eq(200)
589
+ expect(last_response.body).to eq('NilClass')
590
+ end
591
+ end
592
+
593
+ context 'variant-member-type collections' do
594
+ [
595
+ Array[Integer, String],
596
+ [Integer, String, Array[Integer, String]]
597
+ ].each do |type|
598
+ it "is coerced to nil for type #{type}" do
599
+ subject.params do
600
+ requires :param, type: type
601
+ end
602
+ subject.get '/empty_string' do
603
+ params[:param].class
604
+ end
605
+
606
+ get '/empty_string', param: ''
607
+ expect(last_response.status).to eq(200)
608
+ expect(last_response.body).to eq('NilClass')
609
+ end
610
+ end
611
+ end
612
+ end
613
+ end
427
614
  end
428
615
 
429
616
  context 'using coerce_with' do
@@ -461,6 +648,30 @@ describe Grape::Validations::CoerceValidator do
461
648
  expect(JSON.parse(last_response.body)).to eq(%w[a b c d])
462
649
  end
463
650
 
651
+ it 'parses parameters with Array[Array[String]] type and coerce_with' do
652
+ subject.params do
653
+ requires :values, type: Array[Array[String]], coerce_with: ->(val) { val.is_a?(String) ? [val.split(/,/).map(&:strip)] : val }
654
+ end
655
+ subject.post '/coerce_nested_strings' do
656
+ params[:values]
657
+ end
658
+
659
+ post '/coerce_nested_strings', ::Grape::Json.dump(values: 'a,b,c,d'), 'CONTENT_TYPE' => 'application/json'
660
+ expect(last_response.status).to eq(201)
661
+ expect(JSON.parse(last_response.body)).to eq([%w[a b c d]])
662
+
663
+ post '/coerce_nested_strings', ::Grape::Json.dump(values: [%w[a c], %w[b]]), 'CONTENT_TYPE' => 'application/json'
664
+ expect(last_response.status).to eq(201)
665
+ expect(JSON.parse(last_response.body)).to eq([%w[a c], %w[b]])
666
+
667
+ post '/coerce_nested_strings', ::Grape::Json.dump(values: [[]]), 'CONTENT_TYPE' => 'application/json'
668
+ expect(last_response.status).to eq(201)
669
+ expect(JSON.parse(last_response.body)).to eq([[]])
670
+
671
+ post '/coerce_nested_strings', ::Grape::Json.dump(values: [['a', { bar: 0 }], ['b']]), 'CONTENT_TYPE' => 'application/json'
672
+ expect(last_response.status).to eq(400)
673
+ end
674
+
464
675
  it 'parses parameters with Array[Integer] type' do
465
676
  subject.params do
466
677
  requires :values, type: Array[Integer], coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) }
@@ -752,19 +963,6 @@ describe Grape::Validations::CoerceValidator do
752
963
  expect(last_response.body).to eq('String')
753
964
  end
754
965
 
755
- it 'respects nil values' do
756
- subject.params do
757
- optional :a, types: [File, String]
758
- end
759
- subject.get '/' do
760
- params[:a].class.to_s
761
- end
762
-
763
- get '/', a: nil
764
- expect(last_response.status).to eq(200)
765
- expect(last_response.body).to eq('NilClass')
766
- end
767
-
768
966
  it 'fails when no coercion is possible' do
769
967
  subject.params do
770
968
  requires :a, types: [Boolean, Integer]