grape 0.14.0 → 0.15.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -4
  3. data/Gemfile.lock +13 -13
  4. data/README.md +290 -12
  5. data/UPGRADING.md +68 -1
  6. data/gemfiles/rails_3.gemfile +1 -1
  7. data/lib/grape.rb +8 -2
  8. data/lib/grape/api.rb +40 -34
  9. data/lib/grape/dsl/configuration.rb +2 -115
  10. data/lib/grape/dsl/desc.rb +101 -0
  11. data/lib/grape/dsl/headers.rb +16 -0
  12. data/lib/grape/dsl/helpers.rb +5 -9
  13. data/lib/grape/dsl/inside_route.rb +3 -11
  14. data/lib/grape/dsl/logger.rb +20 -0
  15. data/lib/grape/dsl/parameters.rb +12 -10
  16. data/lib/grape/dsl/request_response.rb +17 -4
  17. data/lib/grape/dsl/routing.rb +24 -7
  18. data/lib/grape/dsl/settings.rb +8 -2
  19. data/lib/grape/endpoint.rb +30 -26
  20. data/lib/grape/error_formatter.rb +31 -0
  21. data/lib/grape/error_formatter/base.rb +0 -28
  22. data/lib/grape/error_formatter/json.rb +13 -2
  23. data/lib/grape/error_formatter/txt.rb +3 -1
  24. data/lib/grape/error_formatter/xml.rb +3 -1
  25. data/lib/grape/exceptions/base.rb +11 -4
  26. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  27. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  28. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  29. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  30. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  31. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  32. data/lib/grape/exceptions/invalid_with_option_for_represent.rb +1 -1
  33. data/lib/grape/exceptions/method_not_allowed.rb +10 -0
  34. data/lib/grape/exceptions/missing_group_type.rb +1 -1
  35. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  36. data/lib/grape/exceptions/missing_option.rb +1 -1
  37. data/lib/grape/exceptions/missing_vendor_option.rb +1 -1
  38. data/lib/grape/exceptions/unknown_options.rb +1 -1
  39. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  40. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  41. data/lib/grape/exceptions/unsupported_group_type.rb +1 -1
  42. data/lib/grape/exceptions/validation.rb +2 -1
  43. data/lib/grape/formatter.rb +31 -0
  44. data/lib/grape/middleware/base.rb +28 -2
  45. data/lib/grape/middleware/error.rb +24 -1
  46. data/lib/grape/middleware/formatter.rb +4 -3
  47. data/lib/grape/middleware/versioner/param.rb +13 -2
  48. data/lib/grape/parser.rb +29 -0
  49. data/lib/grape/util/sendfile_response.rb +19 -0
  50. data/lib/grape/util/strict_hash_configuration.rb +1 -1
  51. data/lib/grape/validations/params_scope.rb +39 -9
  52. data/lib/grape/validations/types.rb +16 -0
  53. data/lib/grape/validations/validators/all_or_none.rb +1 -1
  54. data/lib/grape/validations/validators/allow_blank.rb +2 -2
  55. data/lib/grape/validations/validators/at_least_one_of.rb +1 -1
  56. data/lib/grape/validations/validators/base.rb +26 -0
  57. data/lib/grape/validations/validators/coerce.rb +16 -14
  58. data/lib/grape/validations/validators/default.rb +1 -1
  59. data/lib/grape/validations/validators/exactly_one_of.rb +10 -1
  60. data/lib/grape/validations/validators/mutual_exclusion.rb +1 -1
  61. data/lib/grape/validations/validators/presence.rb +1 -1
  62. data/lib/grape/validations/validators/regexp.rb +2 -2
  63. data/lib/grape/validations/validators/values.rb +2 -2
  64. data/lib/grape/version.rb +1 -1
  65. data/spec/grape/api/custom_validations_spec.rb +156 -21
  66. data/spec/grape/api/namespace_parameters_in_route_spec.rb +38 -0
  67. data/spec/grape/api/optional_parameters_in_route_spec.rb +43 -0
  68. data/spec/grape/api/required_parameters_in_route_spec.rb +37 -0
  69. data/spec/grape/api_spec.rb +118 -60
  70. data/spec/grape/dsl/configuration_spec.rb +0 -75
  71. data/spec/grape/dsl/desc_spec.rb +77 -0
  72. data/spec/grape/dsl/headers_spec.rb +32 -0
  73. data/spec/grape/dsl/inside_route_spec.rb +0 -18
  74. data/spec/grape/dsl/logger_spec.rb +26 -0
  75. data/spec/grape/dsl/parameters_spec.rb +13 -7
  76. data/spec/grape/dsl/request_response_spec.rb +17 -3
  77. data/spec/grape/dsl/routing_spec.rb +8 -1
  78. data/spec/grape/dsl/settings_spec.rb +42 -0
  79. data/spec/grape/endpoint_spec.rb +60 -9
  80. data/spec/grape/exceptions/validation_errors_spec.rb +2 -2
  81. data/spec/grape/exceptions/validation_spec.rb +7 -0
  82. data/spec/grape/integration/rack_sendfile_spec.rb +44 -0
  83. data/spec/grape/middleware/base_spec.rb +100 -0
  84. data/spec/grape/middleware/exception_spec.rb +1 -2
  85. data/spec/grape/middleware/formatter_spec.rb +12 -2
  86. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +1 -1
  87. data/spec/grape/middleware/versioner/header_spec.rb +11 -1
  88. data/spec/grape/middleware/versioner/param_spec.rb +105 -1
  89. data/spec/grape/validations/params_scope_spec.rb +77 -0
  90. data/spec/grape/validations/validators/allow_blank_spec.rb +277 -0
  91. data/spec/grape/validations/validators/coerce_spec.rb +91 -0
  92. data/spec/grape/validations/validators/default_spec.rb +6 -0
  93. data/spec/grape/validations/validators/presence_spec.rb +27 -0
  94. data/spec/grape/validations/validators/regexp_spec.rb +36 -0
  95. data/spec/grape/validations/validators/values_spec.rb +44 -0
  96. data/spec/grape/validations_spec.rb +149 -4
  97. data/spec/spec_helper.rb +1 -0
  98. metadata +26 -5
  99. data/lib/grape/formatter/base.rb +0 -31
  100. data/lib/grape/parser/base.rb +0 -29
  101. data/pkg/grape-0.13.0.gem +0 -0
@@ -93,6 +93,7 @@ describe Grape::Validations::ParamsScope do
93
93
  module ParamsScopeSpec
94
94
  class CustomType
95
95
  attr_reader :value
96
+
96
97
  def self.parse(value)
97
98
  fail if value == 'invalid'
98
99
  new(value)
@@ -173,6 +174,24 @@ describe Grape::Validations::ParamsScope do
173
174
  end
174
175
  end
175
176
 
177
+ context 'when the default is an array' do
178
+ context 'and is the entire range of allowed values' do
179
+ it 'does not raise an exception' do
180
+ expect do
181
+ subject.params { optional :numbers, type: Array[Integer], values: 0..2, default: 0..2 }
182
+ end.to_not raise_error
183
+ end
184
+ end
185
+
186
+ context 'and is a subset of allowed values' do
187
+ it 'does not raise an exception' do
188
+ expect do
189
+ subject.params { optional :numbers, type: Array[Integer], values: [0, 1, 2], default: [1, 0] }
190
+ end.to_not raise_error
191
+ end
192
+ end
193
+ end
194
+
176
195
  context 'when both range endpoints are #kind_of? the type' do
177
196
  it 'accepts values in the range' do
178
197
  subject.params do
@@ -297,6 +316,32 @@ describe Grape::Validations::ParamsScope do
297
316
  expect(last_response.status).to eq(200)
298
317
  end
299
318
 
319
+ it 'applies the validations of multiple parameters' do
320
+ subject.params do
321
+ optional :a, :b
322
+ given :a, :b do
323
+ requires :c
324
+ end
325
+ end
326
+ subject.get('/multiple') { declared(params).to_json }
327
+
328
+ get '/multiple'
329
+ expect(last_response.status).to eq(200)
330
+
331
+ get '/multiple', a: true
332
+ expect(last_response.status).to eq(200)
333
+
334
+ get '/multiple', b: true
335
+ expect(last_response.status).to eq(200)
336
+
337
+ get '/multiple', a: true, b: true
338
+ expect(last_response.status).to eq(400)
339
+ expect(last_response.body).to eq('c is missing')
340
+
341
+ get '/multiple', a: true, b: true, c: true
342
+ expect(last_response.status).to eq(200)
343
+ end
344
+
300
345
  it 'raises an error if the dependent parameter was never specified' do
301
346
  expect do
302
347
  subject.params do
@@ -327,5 +372,37 @@ describe Grape::Validations::ParamsScope do
327
372
  expect(last_response.status).to eq(400)
328
373
  expect(last_response.body).to eq('bar[b] is missing')
329
374
  end
375
+
376
+ it 'includes the nested parameter within #declared(params)' do
377
+ subject.params do
378
+ requires :bar, type: Hash do
379
+ optional :a
380
+ given :a do
381
+ requires :b
382
+ end
383
+ end
384
+ end
385
+ subject.get('/nested') { declared(params).to_json }
386
+
387
+ get '/nested', bar: { a: true, b: 'yes' }
388
+ expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'true', 'b' => 'yes' })
389
+ end
390
+
391
+ it 'includes level 2 nested parameters outside the given within #declared(params)' do
392
+ subject.params do
393
+ requires :bar, type: Hash do
394
+ optional :a
395
+ given :a do
396
+ requires :c, type: Hash do
397
+ requires :b
398
+ end
399
+ end
400
+ end
401
+ end
402
+ subject.get('/nested') { declared(params).to_json }
403
+
404
+ get '/nested', bar: { a: true, c: { b: 'yes' } }
405
+ expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'true', 'c' => { 'b' => 'yes' } })
406
+ end
330
407
  end
331
408
  end
@@ -120,6 +120,123 @@ describe Grape::Validations::AllowBlankValidator do
120
120
  end
121
121
  end
122
122
  get '/disallow_string_value_in_an_optional_hash_group'
123
+
124
+ resources :custom_message do
125
+ params do
126
+ requires :name, allow_blank: { value: false, message: 'has no value' }
127
+ end
128
+ get
129
+
130
+ params do
131
+ optional :name, allow_blank: { value: false, message: 'has no value' }
132
+ end
133
+ get '/disallow_blank_optional_param'
134
+
135
+ params do
136
+ requires :name, allow_blank: true
137
+ end
138
+ get '/allow_blank'
139
+
140
+ params do
141
+ requires :val, type: DateTime, allow_blank: true
142
+ end
143
+ get '/allow_datetime_blank'
144
+
145
+ params do
146
+ requires :val, type: DateTime, allow_blank: { value: false, message: 'has no value' }
147
+ end
148
+ get '/disallow_datetime_blank'
149
+
150
+ params do
151
+ requires :val, type: DateTime
152
+ end
153
+ get '/default_allow_datetime_blank'
154
+
155
+ params do
156
+ requires :val, type: Date, allow_blank: true
157
+ end
158
+ get '/allow_date_blank'
159
+
160
+ params do
161
+ requires :val, type: Integer, allow_blank: true
162
+ end
163
+ get '/allow_integer_blank'
164
+
165
+ params do
166
+ requires :val, type: Float, allow_blank: true
167
+ end
168
+ get '/allow_float_blank'
169
+
170
+ params do
171
+ requires :val, type: Fixnum, allow_blank: true
172
+ end
173
+ get '/allow_fixnum_blank'
174
+
175
+ params do
176
+ requires :val, type: Symbol, allow_blank: true
177
+ end
178
+ get '/allow_symbol_blank'
179
+
180
+ params do
181
+ requires :val, type: Boolean, allow_blank: true
182
+ end
183
+ get '/allow_boolean_blank'
184
+
185
+ params do
186
+ requires :val, type: Boolean, allow_blank: { value: false, message: 'has no value' }
187
+ end
188
+ get '/disallow_boolean_blank'
189
+
190
+ params do
191
+ optional :user, type: Hash do
192
+ requires :name, allow_blank: { value: false, message: 'has no value' }
193
+ end
194
+ end
195
+ get '/disallow_blank_required_param_in_an_optional_group'
196
+
197
+ params do
198
+ optional :user, type: Hash do
199
+ requires :name, type: Date, allow_blank: true
200
+ end
201
+ end
202
+ get '/allow_blank_date_param_in_an_optional_group'
203
+
204
+ params do
205
+ optional :user, type: Hash do
206
+ optional :name, allow_blank: { value: false, message: 'has no value' }
207
+ requires :age
208
+ end
209
+ end
210
+ get '/disallow_blank_optional_param_in_an_optional_group'
211
+
212
+ params do
213
+ requires :user, type: Hash do
214
+ requires :name, allow_blank: { value: false, message: 'has no value' }
215
+ end
216
+ end
217
+ get '/disallow_blank_required_param_in_a_required_group'
218
+
219
+ params do
220
+ requires :user, type: Hash do
221
+ requires :name, allow_blank: { value: false, message: 'has no value' }
222
+ end
223
+ end
224
+ get '/disallow_string_value_in_a_required_hash_group'
225
+
226
+ params do
227
+ requires :user, type: Hash do
228
+ optional :name, allow_blank: { value: false, message: 'has no value' }
229
+ end
230
+ end
231
+ get '/disallow_blank_optional_param_in_a_required_group'
232
+
233
+ params do
234
+ optional :user, type: Hash do
235
+ optional :name, allow_blank: { value: false, message: 'has no value' }
236
+ end
237
+ end
238
+ get '/disallow_string_value_in_an_optional_hash_group'
239
+ end
123
240
  end
124
241
  end
125
242
  end
@@ -154,6 +271,166 @@ describe Grape::Validations::AllowBlankValidator do
154
271
  end
155
272
  end
156
273
 
274
+ context 'custom validation message' do
275
+ context 'with invalid input' do
276
+ it 'refuses empty string' do
277
+ get '/custom_message', name: ''
278
+ expect(last_response.body).to eq('{"error":"name has no value"}')
279
+ end
280
+ it 'refuses empty string for an optional param' do
281
+ get '/custom_message/disallow_blank_optional_param', name: ''
282
+ expect(last_response.body).to eq('{"error":"name has no value"}')
283
+ end
284
+ it 'refuses only whitespaces' do
285
+ get '/custom_message', name: ' '
286
+ expect(last_response.body).to eq('{"error":"name has no value"}')
287
+
288
+ get '/custom_message', name: " \n "
289
+ expect(last_response.body).to eq('{"error":"name has no value"}')
290
+
291
+ get '/custom_message', name: "\n"
292
+ expect(last_response.body).to eq('{"error":"name has no value"}')
293
+ end
294
+
295
+ it 'refuses nil' do
296
+ get '/custom_message', name: nil
297
+ expect(last_response.body).to eq('{"error":"name has no value"}')
298
+ end
299
+ end
300
+
301
+ context 'with valid input' do
302
+ it 'accepts valid input' do
303
+ get '/custom_message', name: 'bob'
304
+ expect(last_response.status).to eq(200)
305
+ end
306
+
307
+ it 'accepts empty input when allow_blank is false' do
308
+ get '/custom_message/allow_blank', name: ''
309
+ expect(last_response.status).to eq(200)
310
+ end
311
+
312
+ it 'accepts empty input' do
313
+ get '/custom_message/default_allow_datetime_blank', val: ''
314
+ expect(last_response.status).to eq(200)
315
+ end
316
+
317
+ it 'accepts empty when datetime allow_blank' do
318
+ get '/custom_message/allow_datetime_blank', val: ''
319
+ expect(last_response.status).to eq(200)
320
+ end
321
+
322
+ it 'accepts empty when date allow_blank' do
323
+ get '/custom_message/allow_date_blank', val: ''
324
+ expect(last_response.status).to eq(200)
325
+ end
326
+
327
+ context 'allow_blank when Numeric' do
328
+ it 'accepts empty when integer allow_blank' do
329
+ get '/custom_message/allow_integer_blank', val: ''
330
+ expect(last_response.status).to eq(200)
331
+ end
332
+
333
+ it 'accepts empty when float allow_blank' do
334
+ get '/custom_message/allow_float_blank', val: ''
335
+ expect(last_response.status).to eq(200)
336
+ end
337
+
338
+ it 'accepts empty when fixnum allow_blank' do
339
+ get '/custom_message/allow_fixnum_blank', val: ''
340
+ expect(last_response.status).to eq(200)
341
+ end
342
+ end
343
+
344
+ it 'accepts empty when symbol allow_blank' do
345
+ get '/custom_message/allow_symbol_blank', val: ''
346
+ expect(last_response.status).to eq(200)
347
+ end
348
+
349
+ it 'accepts empty when boolean allow_blank' do
350
+ get '/custom_message/allow_boolean_blank', val: ''
351
+ expect(last_response.status).to eq(200)
352
+ end
353
+
354
+ it 'accepts false when boolean allow_blank' do
355
+ get '/custom_message/disallow_boolean_blank', val: false
356
+ expect(last_response.status).to eq(200)
357
+ end
358
+ end
359
+
360
+ context 'in an optional group' do
361
+ context 'as a required param' do
362
+ it 'accepts a missing group, even with a disallwed blank param' do
363
+ get '/custom_message/disallow_blank_required_param_in_an_optional_group'
364
+ expect(last_response.status).to eq(200)
365
+ end
366
+
367
+ it 'accepts a nested missing date value' do
368
+ get '/custom_message/allow_blank_date_param_in_an_optional_group', user: { name: '' }
369
+ expect(last_response.status).to eq(200)
370
+ end
371
+
372
+ it 'refuses a blank value in an existing group' do
373
+ get '/custom_message/disallow_blank_required_param_in_an_optional_group', user: { name: '' }
374
+ expect(last_response.status).to eq(400)
375
+ expect(last_response.body).to eq('{"error":"user[name] has no value"}')
376
+ end
377
+ end
378
+
379
+ context 'as an optional param' do
380
+ it 'accepts a missing group, even with a disallwed blank param' do
381
+ get '/custom_message/disallow_blank_optional_param_in_an_optional_group'
382
+ expect(last_response.status).to eq(200)
383
+ end
384
+
385
+ it 'accepts a nested missing optional value' do
386
+ get '/custom_message/disallow_blank_optional_param_in_an_optional_group', user: { age: '29' }
387
+ expect(last_response.status).to eq(200)
388
+ end
389
+
390
+ it 'refuses a blank existing value in an existing scope' do
391
+ get '/custom_message/disallow_blank_optional_param_in_an_optional_group', user: { age: '29', name: '' }
392
+ expect(last_response.status).to eq(400)
393
+ expect(last_response.body).to eq('{"error":"user[name] has no value"}')
394
+ end
395
+ end
396
+ end
397
+
398
+ context 'in a required group' do
399
+ context 'as a required param' do
400
+ it 'refuses a blank value in a required existing group' do
401
+ get '/custom_message/disallow_blank_required_param_in_a_required_group', user: { name: '' }
402
+ expect(last_response.status).to eq(400)
403
+ expect(last_response.body).to eq('{"error":"user[name] has no value"}')
404
+ end
405
+
406
+ it 'refuses a string value in a required hash group' do
407
+ get '/custom_message/disallow_string_value_in_a_required_hash_group', user: ''
408
+ expect(last_response.status).to eq(400)
409
+ expect(last_response.body).to eq('{"error":"user is invalid, user[name] is missing"}')
410
+ end
411
+ end
412
+
413
+ context 'as an optional param' do
414
+ it 'accepts a nested missing value' do
415
+ get '/custom_message/disallow_blank_optional_param_in_a_required_group', user: { age: '29' }
416
+ expect(last_response.status).to eq(200)
417
+ end
418
+
419
+ it 'refuses a blank existing value in an existing scope' do
420
+ get '/custom_message/disallow_blank_optional_param_in_a_required_group', user: { age: '29', name: '' }
421
+ expect(last_response.status).to eq(400)
422
+ expect(last_response.body).to eq('{"error":"user[name] has no value"}')
423
+ end
424
+
425
+ it 'refuses a string value in an optional hash group' do
426
+ get '/custom_message/disallow_string_value_in_an_optional_hash_group', user: ''
427
+ expect(last_response.status).to eq(400)
428
+ expect(last_response.body).to eq('{"error":"user is invalid"}')
429
+ end
430
+ end
431
+ end
432
+ end
433
+
157
434
  context 'valid input' do
158
435
  it 'accepts valid input' do
159
436
  get '/', name: 'bob'
@@ -55,6 +55,62 @@ describe Grape::Validations::CoerceValidator do
55
55
  end
56
56
  end
57
57
 
58
+ context 'with a custom validation message' do
59
+ it 'errors on malformed input' do
60
+ subject.params do
61
+ requires :int, type: { value: Integer, message: 'type cast is invalid' }
62
+ end
63
+ subject.get '/single' do
64
+ 'int works'
65
+ end
66
+
67
+ get '/single', int: '43a'
68
+ expect(last_response.status).to eq(400)
69
+ expect(last_response.body).to eq('int type cast is invalid')
70
+
71
+ get '/single', int: '43'
72
+ expect(last_response.status).to eq(200)
73
+ expect(last_response.body).to eq('int works')
74
+ end
75
+
76
+ context 'on custom coercion rules' do
77
+ before do
78
+ subject.params do
79
+ requires :a, types: { value: [Boolean, String], message: 'type cast is invalid' }, coerce_with: (lambda do |val|
80
+ if val == 'yup'
81
+ true
82
+ elsif val == 'false'
83
+ 0
84
+ else
85
+ val
86
+ end
87
+ end)
88
+ end
89
+ subject.get '/' do
90
+ params[:a].class.to_s
91
+ end
92
+ end
93
+
94
+ it 'respects :coerce_with' do
95
+ get '/', a: 'yup'
96
+ expect(last_response.status).to eq(200)
97
+ expect(last_response.body).to eq('TrueClass')
98
+ end
99
+
100
+ it 'still validates type' do
101
+ get '/', a: 'false'
102
+ expect(last_response.status).to eq(400)
103
+ expect(last_response.body).to eq('a type cast is invalid')
104
+ end
105
+
106
+ it 'performs no additional coercion' do
107
+ get '/', a: 'true'
108
+ expect(last_response.status).to eq(200)
109
+ expect(last_response.body).to eq('String')
110
+ end
111
+ end
112
+ end
113
+
58
114
  it 'error on malformed input' do
59
115
  subject.params do
60
116
  requires :int, type: Integer
@@ -368,6 +424,41 @@ describe Grape::Validations::CoerceValidator do
368
424
  expect(last_response.body).to eq('splines[x] does not have a valid value')
369
425
  end
370
426
 
427
+ it 'works when declared optional' do
428
+ subject.params do
429
+ optional :splines, type: JSON do
430
+ requires :x, type: Integer, values: [1, 2, 3]
431
+ optional :ints, type: Array[Integer]
432
+ optional :obj, type: Hash do
433
+ optional :y
434
+ end
435
+ end
436
+ end
437
+ subject.get '/' do
438
+ if params[:splines].is_a? Hash
439
+ params[:splines][:obj][:y]
440
+ else
441
+ 'arrays work' if params[:splines].any? { |s| s.key? :obj }
442
+ end
443
+ end
444
+
445
+ get '/', splines: '{"x":1,"ints":[1,2,3],"obj":{"y":"woof"}}'
446
+ expect(last_response.status).to eq(200)
447
+ expect(last_response.body).to eq('woof')
448
+
449
+ get '/', splines: '[{"x":2,"ints":[]},{"x":3,"ints":[4],"obj":{"y":"quack"}}]'
450
+ expect(last_response.status).to eq(200)
451
+ expect(last_response.body).to eq('arrays work')
452
+
453
+ get '/', splines: '{"x":4,"ints":[2]}'
454
+ expect(last_response.status).to eq(400)
455
+ expect(last_response.body).to eq('splines[x] does not have a valid value')
456
+
457
+ get '/', splines: '[{"x":1,"ints":[]},{"x":4,"ints":[]}]'
458
+ expect(last_response.status).to eq(400)
459
+ expect(last_response.body).to eq('splines[x] does not have a valid value')
460
+ end
461
+
371
462
  it 'accepts Array[JSON] shorthand' do
372
463
  subject.params do
373
464
  requires :splines, type: Array[JSON] do