grape 1.3.0 → 1.3.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +4 -6
  4. data/UPGRADING.md +61 -16
  5. data/lib/grape/api/instance.rb +11 -7
  6. data/lib/grape/api.rb +2 -2
  7. data/lib/grape/content_types.rb +34 -0
  8. data/lib/grape/dsl/helpers.rb +1 -1
  9. data/lib/grape/dsl/inside_route.rb +10 -9
  10. data/lib/grape/dsl/parameters.rb +4 -4
  11. data/lib/grape/dsl/routing.rb +6 -4
  12. data/lib/grape/exceptions/base.rb +0 -4
  13. data/lib/grape/exceptions/validation_errors.rb +11 -12
  14. data/lib/grape/http/headers.rb +26 -0
  15. data/lib/grape/middleware/base.rb +1 -3
  16. data/lib/grape/middleware/stack.rb +2 -1
  17. data/lib/grape/middleware/versioner/header.rb +4 -4
  18. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
  19. data/lib/grape/middleware/versioner/path.rb +1 -1
  20. data/lib/grape/namespace.rb +12 -2
  21. data/lib/grape/path.rb +13 -3
  22. data/lib/grape/request.rb +12 -7
  23. data/lib/grape/router/pattern.rb +17 -16
  24. data/lib/grape/router/route.rb +4 -5
  25. data/lib/grape/router.rb +24 -14
  26. data/lib/grape/util/base_inheritable.rb +13 -6
  27. data/lib/grape/util/cache.rb +20 -0
  28. data/lib/grape/util/lazy_object.rb +43 -0
  29. data/lib/grape/util/reverse_stackable_values.rb +2 -0
  30. data/lib/grape/util/stackable_values.rb +7 -20
  31. data/lib/grape/validations/params_scope.rb +1 -1
  32. data/lib/grape/validations/types/build_coercer.rb +4 -3
  33. data/lib/grape/validations/types/custom_type_coercer.rb +1 -1
  34. data/lib/grape/validations/types/file.rb +15 -12
  35. data/lib/grape/validations/types/json.rb +40 -36
  36. data/lib/grape/validations/types/primitive_coercer.rb +6 -3
  37. data/lib/grape/validations/types.rb +6 -5
  38. data/lib/grape/validations/validators/coerce.rb +14 -12
  39. data/lib/grape/validations/validators/exactly_one_of.rb +4 -2
  40. data/lib/grape/validations/validators/regexp.rb +1 -1
  41. data/lib/grape/version.rb +1 -1
  42. data/lib/grape.rb +2 -3
  43. data/spec/grape/api_spec.rb +7 -6
  44. data/spec/grape/exceptions/validation_errors_spec.rb +2 -2
  45. data/spec/grape/middleware/formatter_spec.rb +2 -2
  46. data/spec/grape/middleware/stack_spec.rb +9 -0
  47. data/spec/grape/path_spec.rb +4 -4
  48. data/spec/grape/validations/instance_behaivour_spec.rb +1 -1
  49. data/spec/grape/validations/types/primitive_coercer_spec.rb +75 -0
  50. data/spec/grape/validations/types_spec.rb +1 -1
  51. data/spec/grape/validations/validators/coerce_spec.rb +160 -78
  52. data/spec/grape/validations/validators/exactly_one_of_spec.rb +12 -12
  53. data/spec/grape/validations_spec.rb +8 -12
  54. data/spec/spec_helper.rb +3 -0
  55. data/spec/support/eager_load.rb +19 -0
  56. metadata +12 -6
  57. data/lib/grape/util/content_types.rb +0 -28
@@ -42,7 +42,6 @@ module Grape
42
42
  Grape::API::Boolean,
43
43
  String,
44
44
  Symbol,
45
- Rack::Multipart::UploadedFile,
46
45
  TrueClass,
47
46
  FalseClass
48
47
  ].freeze
@@ -54,8 +53,7 @@ module Grape
54
53
  Set
55
54
  ].freeze
56
55
 
57
- # Types for which Grape provides special coercion
58
- # and type-checking logic.
56
+ # Special custom types provided by Grape.
59
57
  SPECIAL = {
60
58
  JSON => Json,
61
59
  Array[JSON] => JsonArray,
@@ -130,7 +128,6 @@ module Grape
130
128
  !primitive?(type) &&
131
129
  !structure?(type) &&
132
130
  !multiple?(type) &&
133
- !special?(type) &&
134
131
  type.respond_to?(:parse) &&
135
132
  type.method(:parse).arity == 1
136
133
  end
@@ -143,7 +140,11 @@ module Grape
143
140
  def self.collection_of_custom?(type)
144
141
  (type.is_a?(Array) || type.is_a?(Set)) &&
145
142
  type.length == 1 &&
146
- custom?(type.first)
143
+ (custom?(type.first) || special?(type.first))
144
+ end
145
+
146
+ def self.map_special(type)
147
+ SPECIAL.fetch(type, type)
147
148
  end
148
149
  end
149
150
  end
@@ -47,7 +47,9 @@ module Grape
47
47
  # h[:list] = list
48
48
  # h
49
49
  # => #<Hashie::Mash list=[1, 2, 3, 4]>
50
- params[attr_name] = new_value unless params[attr_name] == new_value
50
+ return if params[attr_name].class == new_value.class && params[attr_name] == new_value
51
+
52
+ params[attr_name] = new_value
51
53
  end
52
54
 
53
55
  private
@@ -65,21 +67,21 @@ module Grape
65
67
  end
66
68
 
67
69
  def coerce_value(val)
68
- # define default values for structures, the dry-types lib which is used
69
- # for coercion doesn't accept nil as a value, so it would fail
70
- if val.nil?
71
- return [] if type == Array || type.is_a?(Array)
72
- return Set.new if type == Set
73
- return {} if type == Hash
74
- end
75
-
76
- converter.call(val)
77
-
70
+ val.nil? ? coerce_nil(val) : converter.call(val)
78
71
  # Some custom types might fail, so it should be treated as an invalid value
79
- rescue
72
+ rescue StandardError
80
73
  Types::InvalidValue.new
81
74
  end
82
75
 
76
+ def coerce_nil(val)
77
+ # define default values for structures, the dry-types lib which is used
78
+ # for coercion doesn't accept nil as a value, so it would fail
79
+ return [] if type == Array
80
+ return Set.new if type == Set
81
+ return {} if type == Hash
82
+ val
83
+ end
84
+
83
85
  # Type to which the parameter will be coerced.
84
86
  #
85
87
  # @return [Class]
@@ -6,8 +6,10 @@ module Grape
6
6
  module Validations
7
7
  class ExactlyOneOfValidator < MultipleParamsBase
8
8
  def validate_params!(params)
9
- return if keys_in_common(params).length == 1
10
- raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:exactly_one))
9
+ keys = keys_in_common(params)
10
+ return if keys.length == 1
11
+ raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:exactly_one)) if keys.length.zero?
12
+ raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion))
11
13
  end
12
14
  end
13
15
  end
@@ -5,7 +5,7 @@ module Grape
5
5
  class RegexpValidator < Base
6
6
  def validate_param!(attr_name, params)
7
7
  return unless params.respond_to?(:key?) && params.key?(attr_name)
8
- return if Array.wrap(params[attr_name]).all? { |param| param.nil? || (param.to_s =~ (options_key?(:value) ? @option[:value] : @option)) }
8
+ return if Array.wrap(params[attr_name]).all? { |param| param.nil? || param.to_s.match?((options_key?(:value) ? @option[:value] : @option)) }
9
9
  raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:regexp))
10
10
  end
11
11
  end
data/lib/grape/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Grape
4
4
  # The current version of Grape.
5
- VERSION = '1.3.0'
5
+ VERSION = '1.3.2'
6
6
  end
data/lib/grape.rb CHANGED
@@ -20,7 +20,6 @@ require 'active_support/core_ext/hash/conversions'
20
20
  require 'active_support/dependencies/autoload'
21
21
  require 'active_support/notifications'
22
22
  require 'i18n'
23
- require 'thread'
24
23
 
25
24
  I18n.load_path << File.expand_path('../grape/locale/en.yml', __FILE__)
26
25
 
@@ -84,7 +83,6 @@ module Grape
84
83
  eager_autoload do
85
84
  autoload :DeepMergeableHash
86
85
  autoload :DeepSymbolizeHash
87
- autoload :DeepHashWithIndifferentAccess
88
86
  autoload :Hash
89
87
  end
90
88
  module ActiveSupport
@@ -219,7 +217,8 @@ module Grape
219
217
  end
220
218
 
221
219
  require 'grape/config'
222
- require 'grape/util/content_types'
220
+ require 'grape/content_types'
221
+
223
222
  require 'grape/util/lazy_value'
224
223
  require 'grape/util/lazy_block'
225
224
  require 'grape/util/endpoint_configuration'
@@ -935,7 +935,7 @@ XML
935
935
 
936
936
  it 'adds a before filter to current and child namespaces only' do
937
937
  subject.get '/' do
938
- "root - #{@foo}"
938
+ "root - #{instance_variable_defined?(:@foo) ? @foo : nil}"
939
939
  end
940
940
  subject.namespace :blah do
941
941
  before { @foo = 'foo' }
@@ -3677,12 +3677,13 @@ XML
3677
3677
  end
3678
3678
  end
3679
3679
  context ':serializable_hash' do
3680
- before(:each) do
3681
- class SerializableHashExample
3682
- def serializable_hash
3683
- { abc: 'def' }
3684
- end
3680
+ class SerializableHashExample
3681
+ def serializable_hash
3682
+ { abc: 'def' }
3685
3683
  end
3684
+ end
3685
+
3686
+ before(:each) do
3686
3687
  subject.format :serializable_hash
3687
3688
  end
3688
3689
  it 'instance' do
@@ -81,8 +81,8 @@ describe Grape::Exceptions::ValidationErrors do
81
81
  expect(last_response.status).to eq(400)
82
82
  expect(JSON.parse(last_response.body)).to eq(
83
83
  [
84
- 'params' => %w[beer wine juice],
85
- 'messages' => ['are missing, exactly one parameter must be provided']
84
+ 'params' => %w[beer wine],
85
+ 'messages' => ['are mutually exclusive']
86
86
  ]
87
87
  )
88
88
  end
@@ -402,10 +402,10 @@ describe Grape::Middleware::Formatter do
402
402
  let(:app) { ->(_env) { [200, {}, ['']] } }
403
403
  before do
404
404
  Grape::Formatter.register :invalid, InvalidFormatter
405
- Grape::ContentTypes::CONTENT_TYPES[:invalid] = 'application/x-invalid'
405
+ Grape::ContentTypes.register :invalid, 'application/x-invalid'
406
406
  end
407
407
  after do
408
- Grape::ContentTypes::CONTENT_TYPES.delete(:invalid)
408
+ Grape::ContentTypes.default_elements.delete(:invalid)
409
409
  Grape::Formatter.default_elements.delete(:invalid)
410
410
  end
411
411
 
@@ -111,6 +111,15 @@ describe Grape::Middleware::Stack do
111
111
  expect(subject[1]).to eq(StackSpec::BlockMiddleware)
112
112
  expect(subject[2]).to eq(StackSpec::BarMiddleware)
113
113
  end
114
+
115
+ context 'middleware spec with proc declaration exists' do
116
+ let(:middleware_spec_with_proc) { [:use, StackSpec::FooMiddleware, proc] }
117
+
118
+ it 'properly forwards spec arguments' do
119
+ expect(subject).to receive(:use).with(StackSpec::FooMiddleware)
120
+ subject.merge_with([middleware_spec_with_proc])
121
+ end
122
+ end
114
123
  end
115
124
 
116
125
  describe '#build' do
@@ -87,12 +87,12 @@ module Grape
87
87
  describe '#namespace?' do
88
88
  it 'is false when the namespace is nil' do
89
89
  path = Path.new(anything, nil, anything)
90
- expect(path.namespace?).to be nil
90
+ expect(path.namespace?).to be_falsey
91
91
  end
92
92
 
93
93
  it 'is false when the namespace starts with whitespace' do
94
94
  path = Path.new(anything, ' /foo', anything)
95
- expect(path.namespace?).to be nil
95
+ expect(path.namespace?).to be_falsey
96
96
  end
97
97
 
98
98
  it 'is false when the namespace is the root path' do
@@ -109,12 +109,12 @@ module Grape
109
109
  describe '#path?' do
110
110
  it 'is false when the path is nil' do
111
111
  path = Path.new(nil, anything, anything)
112
- expect(path.path?).to be nil
112
+ expect(path.path?).to be_falsey
113
113
  end
114
114
 
115
115
  it 'is false when the path starts with whitespace' do
116
116
  path = Path.new(' /foo', anything, anything)
117
- expect(path.path?).to be nil
117
+ expect(path.path?).to be_falsey
118
118
  end
119
119
 
120
120
  it 'is false when the path is the root path' do
@@ -6,7 +6,7 @@ describe 'Validator with instance variables' do
6
6
  let(:validator_type) do
7
7
  Class.new(Grape::Validations::Base) do
8
8
  def validate_param!(_attr_name, _params)
9
- if @instance_variable
9
+ if instance_variable_defined?(:@instance_variable) && @instance_variable
10
10
  raise Grape::Exceptions::Validation.new(params: ['params'],
11
11
  message: 'This should never happen')
12
12
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Grape::Validations::Types::PrimitiveCoercer do
6
+ let(:strict) { false }
7
+
8
+ subject { described_class.new(type, strict) }
9
+
10
+ describe '.call' do
11
+ context 'Boolean' do
12
+ let(:type) { Grape::API::Boolean }
13
+
14
+ [true, 'true', 1].each do |val|
15
+ it "coerces '#{val}' to true" do
16
+ expect(subject.call(val)).to eq(true)
17
+ end
18
+ end
19
+
20
+ [false, 'false', 0].each do |val|
21
+ it "coerces '#{val}' to false" do
22
+ expect(subject.call(val)).to eq(false)
23
+ end
24
+ end
25
+
26
+ it 'returns an error when the given value cannot be coerced' do
27
+ expect(subject.call(123)).to be_instance_of(Grape::Validations::Types::InvalidValue)
28
+ end
29
+ end
30
+
31
+ context 'String' do
32
+ let(:type) { String }
33
+
34
+ it 'coerces to String' do
35
+ expect(subject.call(10)).to eq('10')
36
+ end
37
+ end
38
+
39
+ context 'BigDecimal' do
40
+ let(:type) { BigDecimal }
41
+
42
+ it 'coerces to BigDecimal' do
43
+ expect(subject.call(5)).to eq(BigDecimal(5))
44
+ end
45
+ end
46
+
47
+ context 'the strict mode' do
48
+ let(:strict) { true }
49
+
50
+ context 'Boolean' do
51
+ let(:type) { Grape::API::Boolean }
52
+
53
+ it 'returns an error when the given value is not Boolean' do
54
+ expect(subject.call(1)).to be_instance_of(Grape::Validations::Types::InvalidValue)
55
+ end
56
+
57
+ it 'returns a value as it is when the given value is Boolean' do
58
+ expect(subject.call(true)).to eq(true)
59
+ end
60
+ end
61
+
62
+ context 'BigDecimal' do
63
+ let(:type) { BigDecimal }
64
+
65
+ it 'returns an error when the given value is not BigDecimal' do
66
+ expect(subject.call(1)).to be_instance_of(Grape::Validations::Types::InvalidValue)
67
+ end
68
+
69
+ it 'returns a value as it is when the given value is BigDecimal' do
70
+ expect(subject.call(BigDecimal(0))).to eq(BigDecimal(0))
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -17,7 +17,7 @@ describe Grape::Validations::Types do
17
17
  [
18
18
  Integer, Float, Numeric, BigDecimal,
19
19
  Grape::API::Boolean, String, Symbol,
20
- Date, DateTime, Time, Rack::Multipart::UploadedFile
20
+ Date, DateTime, Time
21
21
  ].each do |type|
22
22
  it "recognizes #{type} as a primitive" do
23
23
  expect(described_class.primitive?(type)).to be_truthy
@@ -154,6 +154,49 @@ describe Grape::Validations::CoerceValidator do
154
154
  end
155
155
 
156
156
  context 'coerces' do
157
+ context 'json' do
158
+ let(:headers) { { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' } }
159
+
160
+ it 'BigDecimal' do
161
+ subject.params do
162
+ requires :bigdecimal, type: BigDecimal
163
+ end
164
+ subject.post '/bigdecimal' do
165
+ "#{params[:bigdecimal].class} #{params[:bigdecimal].to_f}"
166
+ end
167
+
168
+ post '/bigdecimal', { bigdecimal: 45.1 }.to_json, headers
169
+ expect(last_response.status).to eq(201)
170
+ expect(last_response.body).to eq('BigDecimal 45.1')
171
+ end
172
+
173
+ it 'Boolean' do
174
+ subject.params do
175
+ requires :boolean, type: Boolean
176
+ end
177
+ subject.post '/boolean' do
178
+ params[:boolean]
179
+ end
180
+
181
+ post '/boolean', { boolean: 'true' }.to_json, headers
182
+ expect(last_response.status).to eq(201)
183
+ expect(last_response.body).to eq('true')
184
+ end
185
+ end
186
+
187
+ it 'BigDecimal' do
188
+ subject.params do
189
+ requires :bigdecimal, coerce: BigDecimal
190
+ end
191
+ subject.get '/bigdecimal' do
192
+ params[:bigdecimal].class
193
+ end
194
+
195
+ get '/bigdecimal', bigdecimal: '45'
196
+ expect(last_response.status).to eq(200)
197
+ expect(last_response.body).to eq('BigDecimal')
198
+ end
199
+
157
200
  it 'Integer' do
158
201
  subject.params do
159
202
  requires :int, coerce: Integer
@@ -167,6 +210,23 @@ describe Grape::Validations::CoerceValidator do
167
210
  expect(last_response.body).to eq(integer_class_name)
168
211
  end
169
212
 
213
+ it 'String' do
214
+ subject.params do
215
+ requires :string, coerce: String
216
+ end
217
+ subject.get '/string' do
218
+ params[:string].class
219
+ end
220
+
221
+ get '/string', string: 45
222
+ expect(last_response.status).to eq(200)
223
+ expect(last_response.body).to eq('String')
224
+
225
+ get '/string', string: nil
226
+ expect(last_response.status).to eq(200)
227
+ expect(last_response.body).to eq('NilClass')
228
+ end
229
+
170
230
  it 'is a custom type' do
171
231
  subject.params do
172
232
  requires :uri, coerce: SecureURIOnly
@@ -281,104 +341,73 @@ describe Grape::Validations::CoerceValidator do
281
341
  end
282
342
  end
283
343
 
284
- it 'Bool' do
285
- subject.params do
286
- requires :bool, coerce: Grape::API::Boolean
287
- end
288
- subject.get '/bool' do
289
- params[:bool].class
290
- end
291
-
292
- get '/bool', bool: 1
293
- expect(last_response.status).to eq(200)
294
- expect(last_response.body).to eq('TrueClass')
295
-
296
- get '/bool', bool: 0
297
- expect(last_response.status).to eq(200)
298
- expect(last_response.body).to eq('FalseClass')
299
-
300
- get '/bool', bool: 'false'
301
- expect(last_response.status).to eq(200)
302
- expect(last_response.body).to eq('FalseClass')
303
-
304
- get '/bool', bool: 'true'
305
- expect(last_response.status).to eq(200)
306
- expect(last_response.body).to eq('TrueClass')
307
- end
308
-
309
344
  it 'Boolean' do
310
345
  subject.params do
311
- optional :boolean, type: Boolean, default: true
346
+ requires :boolean, type: Boolean
312
347
  end
313
348
  subject.get '/boolean' do
314
349
  params[:boolean].class
315
350
  end
316
351
 
317
- get '/boolean'
318
- expect(last_response.status).to eq(200)
319
- expect(last_response.body).to eq('TrueClass')
320
-
321
- get '/boolean', boolean: true
322
- expect(last_response.status).to eq(200)
323
- expect(last_response.body).to eq('TrueClass')
324
-
325
- get '/boolean', boolean: false
326
- expect(last_response.status).to eq(200)
327
- expect(last_response.body).to eq('FalseClass')
328
-
329
- get '/boolean', boolean: 'true'
352
+ get '/boolean', boolean: 1
330
353
  expect(last_response.status).to eq(200)
331
354
  expect(last_response.body).to eq('TrueClass')
355
+ end
332
356
 
333
- get '/boolean', boolean: 'false'
334
- expect(last_response.status).to eq(200)
335
- expect(last_response.body).to eq('FalseClass')
357
+ context 'File' do
358
+ let(:file) { Rack::Test::UploadedFile.new(__FILE__) }
359
+ let(:filename) { File.basename(__FILE__).to_s }
336
360
 
337
- get '/boolean', boolean: 123
338
- expect(last_response.status).to eq(400)
339
- expect(last_response.body).to eq('boolean is invalid')
361
+ it 'Rack::Multipart::UploadedFile' do
362
+ subject.params do
363
+ requires :file, type: Rack::Multipart::UploadedFile
364
+ end
365
+ subject.post '/upload' do
366
+ params[:file][:filename]
367
+ end
340
368
 
341
- get '/boolean', boolean: '123'
342
- expect(last_response.status).to eq(400)
343
- expect(last_response.body).to eq('boolean is invalid')
344
- end
369
+ post '/upload', file: file
370
+ expect(last_response.status).to eq(201)
371
+ expect(last_response.body).to eq(filename)
345
372
 
346
- it 'Rack::Multipart::UploadedFile' do
347
- subject.params do
348
- requires :file, type: Rack::Multipart::UploadedFile
349
- end
350
- subject.post '/upload' do
351
- params[:file][:filename]
373
+ post '/upload', file: 'not a file'
374
+ expect(last_response.status).to eq(400)
375
+ expect(last_response.body).to eq('file is invalid')
352
376
  end
353
377
 
354
- post '/upload', file: Rack::Test::UploadedFile.new(__FILE__)
355
- expect(last_response.status).to eq(201)
356
- expect(last_response.body).to eq(File.basename(__FILE__).to_s)
378
+ it 'File' do
379
+ subject.params do
380
+ requires :file, coerce: File
381
+ end
382
+ subject.post '/upload' do
383
+ params[:file][:filename]
384
+ end
357
385
 
358
- post '/upload', file: 'not a file'
359
- expect(last_response.status).to eq(400)
360
- expect(last_response.body).to eq('file is invalid')
361
- end
386
+ post '/upload', file: file
387
+ expect(last_response.status).to eq(201)
388
+ expect(last_response.body).to eq(filename)
362
389
 
363
- it 'File' do
364
- subject.params do
365
- requires :file, coerce: File
366
- end
367
- subject.post '/upload' do
368
- params[:file][:filename]
369
- end
390
+ post '/upload', file: 'not a file'
391
+ expect(last_response.status).to eq(400)
392
+ expect(last_response.body).to eq('file is invalid')
370
393
 
371
- post '/upload', file: Rack::Test::UploadedFile.new(__FILE__)
372
- expect(last_response.status).to eq(201)
373
- expect(last_response.body).to eq(File.basename(__FILE__).to_s)
394
+ post '/upload', file: { filename: 'fake file', tempfile: '/etc/passwd' }
395
+ expect(last_response.status).to eq(400)
396
+ expect(last_response.body).to eq('file is invalid')
397
+ end
374
398
 
375
- post '/upload', file: 'not a file'
376
- expect(last_response.status).to eq(400)
377
- expect(last_response.body).to eq('file is invalid')
399
+ it 'collection' do
400
+ subject.params do
401
+ requires :files, type: Array[File]
402
+ end
403
+ subject.post '/upload' do
404
+ params[:files].first[:filename]
405
+ end
378
406
 
379
- post '/upload', file: { filename: 'fake file', tempfile: '/etc/passwd' }
380
- expect(last_response.status).to eq(400)
381
- expect(last_response.body).to eq('file is invalid')
407
+ post '/upload', files: [file]
408
+ expect(last_response.status).to eq(201)
409
+ expect(last_response.body).to eq(filename)
410
+ end
382
411
  end
383
412
 
384
413
  it 'Nests integers' do
@@ -514,6 +543,46 @@ describe Grape::Validations::CoerceValidator do
514
543
  expect(last_response.body).to eq('3')
515
544
  end
516
545
 
546
+ context 'Integer type and coerce_with potentially returning nil' do
547
+ before do
548
+ subject.params do
549
+ requires :int, type: Integer, coerce_with: (lambda do |val|
550
+ if val == '0'
551
+ nil
552
+ elsif val.match?(/^-?\d+$/)
553
+ val.to_i
554
+ else
555
+ val
556
+ end
557
+ end)
558
+ end
559
+ subject.get '/' do
560
+ params[:int].class.to_s
561
+ end
562
+ end
563
+
564
+ it 'accepts value that coerces to nil' do
565
+ get '/', int: '0'
566
+
567
+ expect(last_response.status).to eq(200)
568
+ expect(last_response.body).to eq('NilClass')
569
+ end
570
+
571
+ it 'coerces to Integer' do
572
+ get '/', int: '1'
573
+
574
+ expect(last_response.status).to eq(200)
575
+ expect(last_response.body).to eq('Integer')
576
+ end
577
+
578
+ it 'returns invalid value if coercion returns a wrong type' do
579
+ get '/', int: 'lol'
580
+
581
+ expect(last_response.status).to eq(400)
582
+ expect(last_response.body).to eq('int is invalid')
583
+ end
584
+ end
585
+
517
586
  it 'must be supplied with :type or :coerce' do
518
587
  expect do
519
588
  subject.params do
@@ -683,6 +752,19 @@ describe Grape::Validations::CoerceValidator do
683
752
  expect(last_response.body).to eq('String')
684
753
  end
685
754
 
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
+
686
768
  it 'fails when no coercion is possible' do
687
769
  subject.params do
688
770
  requires :a, types: [Boolean, Integer]