grape 1.3.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +90 -0
  3. data/LICENSE +1 -1
  4. data/README.md +104 -21
  5. data/UPGRADING.md +243 -39
  6. data/lib/grape.rb +4 -5
  7. data/lib/grape/api.rb +4 -4
  8. data/lib/grape/api/instance.rb +32 -31
  9. data/lib/grape/content_types.rb +34 -0
  10. data/lib/grape/dsl/helpers.rb +2 -1
  11. data/lib/grape/dsl/inside_route.rb +76 -42
  12. data/lib/grape/dsl/parameters.rb +4 -4
  13. data/lib/grape/dsl/routing.rb +8 -8
  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/base.rb +0 -4
  18. data/lib/grape/exceptions/validation_errors.rb +11 -12
  19. data/lib/grape/http/headers.rb +26 -0
  20. data/lib/grape/middleware/base.rb +3 -4
  21. data/lib/grape/middleware/error.rb +10 -12
  22. data/lib/grape/middleware/formatter.rb +3 -3
  23. data/lib/grape/middleware/stack.rb +19 -5
  24. data/lib/grape/middleware/versioner/header.rb +4 -4
  25. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
  26. data/lib/grape/middleware/versioner/path.rb +1 -1
  27. data/lib/grape/namespace.rb +12 -2
  28. data/lib/grape/path.rb +13 -3
  29. data/lib/grape/request.rb +13 -8
  30. data/lib/grape/router.rb +26 -30
  31. data/lib/grape/router/attribute_translator.rb +25 -4
  32. data/lib/grape/router/pattern.rb +17 -16
  33. data/lib/grape/router/route.rb +5 -24
  34. data/lib/grape/{serve_file → serve_stream}/file_body.rb +1 -1
  35. data/lib/grape/{serve_file → serve_stream}/sendfile_response.rb +1 -1
  36. data/lib/grape/{serve_file/file_response.rb → serve_stream/stream_response.rb} +8 -8
  37. data/lib/grape/util/base_inheritable.rb +15 -8
  38. data/lib/grape/util/cache.rb +20 -0
  39. data/lib/grape/util/lazy_object.rb +43 -0
  40. data/lib/grape/util/lazy_value.rb +1 -0
  41. data/lib/grape/util/reverse_stackable_values.rb +2 -0
  42. data/lib/grape/util/stackable_values.rb +7 -20
  43. data/lib/grape/validations/params_scope.rb +6 -5
  44. data/lib/grape/validations/types.rb +6 -5
  45. data/lib/grape/validations/types/array_coercer.rb +14 -5
  46. data/lib/grape/validations/types/build_coercer.rb +5 -8
  47. data/lib/grape/validations/types/custom_type_coercer.rb +14 -2
  48. data/lib/grape/validations/types/dry_type_coercer.rb +36 -1
  49. data/lib/grape/validations/types/file.rb +15 -12
  50. data/lib/grape/validations/types/json.rb +40 -36
  51. data/lib/grape/validations/types/primitive_coercer.rb +15 -6
  52. data/lib/grape/validations/types/set_coercer.rb +6 -4
  53. data/lib/grape/validations/types/variant_collection_coercer.rb +1 -1
  54. data/lib/grape/validations/validators/as.rb +1 -1
  55. data/lib/grape/validations/validators/base.rb +2 -4
  56. data/lib/grape/validations/validators/coerce.rb +4 -11
  57. data/lib/grape/validations/validators/default.rb +3 -5
  58. data/lib/grape/validations/validators/exactly_one_of.rb +4 -2
  59. data/lib/grape/validations/validators/except_values.rb +1 -1
  60. data/lib/grape/validations/validators/regexp.rb +1 -1
  61. data/lib/grape/validations/validators/values.rb +1 -1
  62. data/lib/grape/version.rb +1 -1
  63. data/spec/grape/api/instance_spec.rb +50 -0
  64. data/spec/grape/api_spec.rb +82 -6
  65. data/spec/grape/dsl/inside_route_spec.rb +182 -33
  66. data/spec/grape/endpoint/declared_spec.rb +590 -0
  67. data/spec/grape/endpoint_spec.rb +0 -521
  68. data/spec/grape/entity_spec.rb +6 -0
  69. data/spec/grape/exceptions/validation_errors_spec.rb +2 -2
  70. data/spec/grape/integration/rack_sendfile_spec.rb +12 -8
  71. data/spec/grape/middleware/auth/strategies_spec.rb +1 -1
  72. data/spec/grape/middleware/error_spec.rb +1 -1
  73. data/spec/grape/middleware/formatter_spec.rb +3 -3
  74. data/spec/grape/middleware/stack_spec.rb +12 -1
  75. data/spec/grape/path_spec.rb +4 -4
  76. data/spec/grape/validations/instance_behaivour_spec.rb +1 -1
  77. data/spec/grape/validations/params_scope_spec.rb +26 -0
  78. data/spec/grape/validations/types/array_coercer_spec.rb +35 -0
  79. data/spec/grape/validations/types/primitive_coercer_spec.rb +135 -0
  80. data/spec/grape/validations/types/set_coercer_spec.rb +34 -0
  81. data/spec/grape/validations/types_spec.rb +1 -1
  82. data/spec/grape/validations/validators/coerce_spec.rb +329 -77
  83. data/spec/grape/validations/validators/default_spec.rb +170 -0
  84. data/spec/grape/validations/validators/exactly_one_of_spec.rb +12 -12
  85. data/spec/grape/validations/validators/except_values_spec.rb +1 -0
  86. data/spec/grape/validations/validators/values_spec.rb +1 -1
  87. data/spec/grape/validations_spec.rb +30 -30
  88. data/spec/integration/eager_load/eager_load_spec.rb +15 -0
  89. data/spec/spec_helper.rb +3 -10
  90. data/spec/support/chunks.rb +14 -0
  91. data/spec/support/eager_load.rb +19 -0
  92. data/spec/support/versioned_helpers.rb +3 -5
  93. metadata +121 -105
  94. data/lib/grape/util/content_types.rb +0 -28
@@ -203,80 +203,229 @@ describe Grape::Endpoint do
203
203
  end
204
204
 
205
205
  describe '#file' do
206
+ before do
207
+ allow(subject).to receive(:warn)
208
+ end
209
+
206
210
  describe 'set' do
207
211
  context 'as file path' do
208
212
  let(:file_path) { '/some/file/path' }
209
213
 
210
- let(:file_response) do
211
- file_body = Grape::ServeFile::FileBody.new(file_path)
212
- Grape::ServeFile::FileResponse.new(file_body)
213
- end
214
+ it 'emits a warning that this method is deprecated' do
215
+ expect(subject).to receive(:warn).with(/Use sendfile or stream/)
214
216
 
215
- before do
216
217
  subject.file file_path
217
218
  end
218
219
 
219
- it 'returns value wrapped in FileResponse' do
220
- expect(subject.file).to eq file_response
220
+ it 'forwards the call to sendfile' do
221
+ expect(subject).to receive(:sendfile).with(file_path)
222
+
223
+ subject.file file_path
221
224
  end
222
225
  end
223
226
 
224
227
  context 'as object (backward compatibility)' do
225
- let(:file_object) { Class.new }
228
+ let(:file_object) { double('StreamerObject', each: nil) }
229
+
230
+ it 'emits a warning that this method is deprecated' do
231
+ expect(subject).to receive(:warn).with(/Use stream to use a Stream object/)
232
+
233
+ subject.file file_object
234
+ end
235
+
236
+ it 'forwards the call to stream' do
237
+ expect(subject).to receive(:stream).with(file_object)
238
+
239
+ subject.file file_object
240
+ end
241
+ end
242
+ end
243
+
244
+ describe 'get' do
245
+ it 'emits a warning that this method is deprecated' do
246
+ expect(subject).to receive(:warn).with(/Use sendfile or stream/)
247
+
248
+ subject.file
249
+ end
250
+
251
+ it 'fowards call to sendfile' do
252
+ expect(subject).to receive(:sendfile)
253
+
254
+ subject.file
255
+ end
256
+ end
257
+ end
258
+
259
+ describe '#sendfile' do
260
+ describe 'set' do
261
+ context 'as file path' do
262
+ let(:file_path) { '/some/file/path' }
226
263
 
227
264
  let(:file_response) do
228
- Grape::ServeFile::FileResponse.new(file_object)
265
+ file_body = Grape::ServeStream::FileBody.new(file_path)
266
+ Grape::ServeStream::StreamResponse.new(file_body)
229
267
  end
230
268
 
231
269
  before do
232
- subject.file file_object
270
+ subject.header 'Cache-Control', 'cache'
271
+ subject.header 'Content-Length', 123
272
+ subject.header 'Transfer-Encoding', 'base64'
273
+ end
274
+
275
+ it 'sends no deprecation warnings' do
276
+ expect(subject).to_not receive(:warn)
277
+
278
+ subject.sendfile file_path
279
+ end
280
+
281
+ it 'returns value wrapped in StreamResponse' do
282
+ subject.sendfile file_path
283
+
284
+ expect(subject.sendfile).to eq file_response
285
+ end
286
+
287
+ it 'does not change the Cache-Control header' do
288
+ subject.sendfile file_path
289
+
290
+ expect(subject.header['Cache-Control']).to eq 'cache'
291
+ end
292
+
293
+ it 'does not change the Content-Length header' do
294
+ subject.sendfile file_path
295
+
296
+ expect(subject.header['Content-Length']).to eq 123
297
+ end
298
+
299
+ it 'does not change the Transfer-Encoding header' do
300
+ subject.sendfile file_path
301
+
302
+ expect(subject.header['Transfer-Encoding']).to eq 'base64'
233
303
  end
304
+ end
305
+
306
+ context 'as object' do
307
+ let(:file_object) { double('StreamerObject', each: nil) }
234
308
 
235
- it 'returns value wrapped in FileResponse' do
236
- expect(subject.file).to eq file_response
309
+ it 'raises an error that only a file path is supported' do
310
+ expect { subject.sendfile file_object }.to raise_error(ArgumentError, /Argument must be a file path/)
237
311
  end
238
312
  end
239
313
  end
240
314
 
241
315
  it 'returns default' do
242
- expect(subject.file).to be nil
316
+ expect(subject.sendfile).to be nil
243
317
  end
244
318
  end
245
319
 
246
320
  describe '#stream' do
247
321
  describe 'set' do
248
- let(:file_object) { Class.new }
322
+ context 'as a file path' do
323
+ let(:file_path) { '/some/file/path' }
249
324
 
250
- before do
251
- subject.header 'Cache-Control', 'cache'
252
- subject.header 'Content-Length', 123
253
- subject.header 'Transfer-Encoding', 'base64'
254
- subject.stream file_object
255
- end
325
+ let(:file_response) do
326
+ file_body = Grape::ServeStream::FileBody.new(file_path)
327
+ Grape::ServeStream::StreamResponse.new(file_body)
328
+ end
256
329
 
257
- it 'returns value wrapped in FileResponse' do
258
- expect(subject.stream).to eq Grape::ServeFile::FileResponse.new(file_object)
259
- end
330
+ before do
331
+ subject.header 'Cache-Control', 'cache'
332
+ subject.header 'Content-Length', 123
333
+ subject.header 'Transfer-Encoding', 'base64'
334
+ end
260
335
 
261
- it 'also sets result of file to value wrapped in FileResponse' do
262
- expect(subject.file).to eq Grape::ServeFile::FileResponse.new(file_object)
263
- end
336
+ it 'emits no deprecation warnings' do
337
+ expect(subject).to_not receive(:warn)
338
+
339
+ subject.stream file_path
340
+ end
341
+
342
+ it 'returns file body wrapped in StreamResponse' do
343
+ subject.stream file_path
344
+
345
+ expect(subject.stream).to eq file_response
346
+ end
347
+
348
+ it 'sets Cache-Control header to no-cache' do
349
+ subject.stream file_path
350
+
351
+ expect(subject.header['Cache-Control']).to eq 'no-cache'
352
+ end
353
+
354
+ it 'does not change Cache-Control header' do
355
+ subject.stream
356
+
357
+ expect(subject.header['Cache-Control']).to eq 'cache'
358
+ end
359
+
360
+ it 'sets Content-Length header to nil' do
361
+ subject.stream file_path
362
+
363
+ expect(subject.header['Content-Length']).to eq nil
364
+ end
365
+
366
+ it 'sets Transfer-Encoding header to nil' do
367
+ subject.stream file_path
264
368
 
265
- it 'sets Cache-Control header to no-cache' do
266
- expect(subject.header['Cache-Control']).to eq 'no-cache'
369
+ expect(subject.header['Transfer-Encoding']).to eq nil
370
+ end
267
371
  end
268
372
 
269
- it 'sets Content-Length header to nil' do
270
- expect(subject.header['Content-Length']).to eq nil
373
+ context 'as a stream object' do
374
+ let(:stream_object) { double('StreamerObject', each: nil) }
375
+
376
+ let(:stream_response) do
377
+ Grape::ServeStream::StreamResponse.new(stream_object)
378
+ end
379
+
380
+ before do
381
+ subject.header 'Cache-Control', 'cache'
382
+ subject.header 'Content-Length', 123
383
+ subject.header 'Transfer-Encoding', 'base64'
384
+ end
385
+
386
+ it 'emits no deprecation warnings' do
387
+ expect(subject).to_not receive(:warn)
388
+
389
+ subject.stream stream_object
390
+ end
391
+
392
+ it 'returns value wrapped in StreamResponse' do
393
+ subject.stream stream_object
394
+
395
+ expect(subject.stream).to eq stream_response
396
+ end
397
+
398
+ it 'sets Cache-Control header to no-cache' do
399
+ subject.stream stream_object
400
+
401
+ expect(subject.header['Cache-Control']).to eq 'no-cache'
402
+ end
403
+
404
+ it 'sets Content-Length header to nil' do
405
+ subject.stream stream_object
406
+
407
+ expect(subject.header['Content-Length']).to eq nil
408
+ end
409
+
410
+ it 'sets Transfer-Encoding header to nil' do
411
+ subject.stream stream_object
412
+
413
+ expect(subject.header['Transfer-Encoding']).to eq nil
414
+ end
271
415
  end
272
416
 
273
- it 'sets Transfer-Encoding header to nil' do
274
- expect(subject.header['Transfer-Encoding']).to eq nil
417
+ context 'as a non-stream object' do
418
+ let(:non_stream_object) { double('NonStreamerObject') }
419
+
420
+ it 'raises an error that the object must implement :each' do
421
+ expect { subject.stream non_stream_object }.to raise_error(ArgumentError, /:each/)
422
+ end
275
423
  end
276
424
  end
277
425
 
278
426
  it 'returns default' do
279
- expect(subject.file).to be nil
427
+ expect(subject.stream).to be nil
428
+ expect(subject.header['Cache-Control']).to eq nil
280
429
  end
281
430
  end
282
431
 
@@ -0,0 +1,590 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Grape::Endpoint do
6
+ subject { Class.new(Grape::API) }
7
+
8
+ def app
9
+ subject
10
+ end
11
+
12
+ describe '#declared' do
13
+ before do
14
+ subject.format :json
15
+ subject.params do
16
+ requires :first
17
+ optional :second
18
+ optional :third, default: 'third-default'
19
+ optional :nested, type: Hash do
20
+ optional :fourth
21
+ optional :fifth
22
+ optional :nested_two, type: Hash do
23
+ optional :sixth
24
+ optional :nested_three, type: Hash do
25
+ optional :seventh
26
+ end
27
+ end
28
+ optional :nested_arr, type: Array do
29
+ optional :eighth
30
+ end
31
+ optional :empty_arr, type: Array
32
+ optional :empty_typed_arr, type: Array[String]
33
+ optional :empty_hash, type: Hash
34
+ optional :empty_set, type: Set
35
+ optional :empty_typed_set, type: Set[String]
36
+ end
37
+ optional :arr, type: Array do
38
+ optional :nineth
39
+ end
40
+ optional :empty_arr, type: Array
41
+ optional :empty_typed_arr, type: Array[String]
42
+ optional :empty_hash, type: Hash
43
+ optional :empty_set, type: Set
44
+ optional :empty_typed_set, type: Set[String]
45
+ end
46
+ end
47
+
48
+ context 'when params are not built with default class' do
49
+ it 'returns an object that corresponds with the params class - hash with indifferent access' do
50
+ subject.params do
51
+ build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder
52
+ end
53
+ subject.get '/declared' do
54
+ d = declared(params, include_missing: true)
55
+ { declared_class: d.class.to_s }
56
+ end
57
+
58
+ get '/declared?first=present'
59
+ expect(JSON.parse(last_response.body)['declared_class']).to eq('ActiveSupport::HashWithIndifferentAccess')
60
+ end
61
+
62
+ it 'returns an object that corresponds with the params class - hashie mash' do
63
+ subject.params do
64
+ build_with Grape::Extensions::Hashie::Mash::ParamBuilder
65
+ end
66
+ subject.get '/declared' do
67
+ d = declared(params, include_missing: true)
68
+ { declared_class: d.class.to_s }
69
+ end
70
+
71
+ get '/declared?first=present'
72
+ expect(JSON.parse(last_response.body)['declared_class']).to eq('Hashie::Mash')
73
+ end
74
+
75
+ it 'returns an object that corresponds with the params class - hash' do
76
+ subject.params do
77
+ build_with Grape::Extensions::Hash::ParamBuilder
78
+ end
79
+ subject.get '/declared' do
80
+ d = declared(params, include_missing: true)
81
+ { declared_class: d.class.to_s }
82
+ end
83
+
84
+ get '/declared?first=present'
85
+ expect(JSON.parse(last_response.body)['declared_class']).to eq('Hash')
86
+ end
87
+ end
88
+
89
+ it 'should show nil for nested params if include_missing is true' do
90
+ subject.get '/declared' do
91
+ declared(params, include_missing: true)
92
+ end
93
+
94
+ get '/declared?first=present'
95
+ expect(last_response.status).to eq(200)
96
+ expect(JSON.parse(last_response.body)['nested']['fourth']).to be_nil
97
+ end
98
+
99
+ it 'does not work in a before filter' do
100
+ subject.before do
101
+ declared(params)
102
+ end
103
+ subject.get('/declared') { declared(params) }
104
+
105
+ expect { get('/declared') }.to raise_error(
106
+ Grape::DSL::InsideRoute::MethodNotYetAvailable
107
+ )
108
+ end
109
+
110
+ it 'has as many keys as there are declared params' do
111
+ subject.get '/declared' do
112
+ declared(params)
113
+ end
114
+ get '/declared?first=present'
115
+ expect(last_response.status).to eq(200)
116
+ expect(JSON.parse(last_response.body).keys.size).to eq(10)
117
+ end
118
+
119
+ it 'has a optional param with default value all the time' do
120
+ subject.get '/declared' do
121
+ declared(params)
122
+ end
123
+ get '/declared?first=one'
124
+ expect(last_response.status).to eq(200)
125
+ expect(JSON.parse(last_response.body)['third']).to eql('third-default')
126
+ end
127
+
128
+ it 'builds nested params' do
129
+ subject.get '/declared' do
130
+ declared(params)
131
+ end
132
+
133
+ get '/declared?first=present&nested[fourth]=1'
134
+ expect(last_response.status).to eq(200)
135
+ expect(JSON.parse(last_response.body)['nested'].keys.size).to eq 9
136
+ end
137
+
138
+ it 'builds arrays correctly' do
139
+ subject.params do
140
+ requires :first
141
+ optional :second, type: Array
142
+ end
143
+ subject.post('/declared') { declared(params) }
144
+
145
+ post '/declared', first: 'present', second: ['present']
146
+ expect(last_response.status).to eq(201)
147
+
148
+ body = JSON.parse(last_response.body)
149
+ expect(body['second']).to eq(['present'])
150
+ end
151
+
152
+ it 'builds nested params when given array' do
153
+ subject.get '/dummy' do
154
+ end
155
+ subject.params do
156
+ requires :first
157
+ optional :second
158
+ optional :third, default: 'third-default'
159
+ optional :nested, type: Array do
160
+ optional :fourth
161
+ end
162
+ end
163
+ subject.get '/declared' do
164
+ declared(params)
165
+ end
166
+
167
+ get '/declared?first=present&nested[][fourth]=1&nested[][fourth]=2'
168
+ expect(last_response.status).to eq(200)
169
+ expect(JSON.parse(last_response.body)['nested'].size).to eq 2
170
+ end
171
+
172
+ context 'when the param is missing and include_missing=false' do
173
+ before do
174
+ subject.get('/declared') { declared(params, include_missing: false) }
175
+ end
176
+
177
+ it 'sets nested objects to be nil' do
178
+ get '/declared?first=present'
179
+ expect(last_response.status).to eq(200)
180
+ expect(JSON.parse(last_response.body)['nested']).to be_nil
181
+ end
182
+ end
183
+
184
+ context 'when the param is missing and include_missing=true' do
185
+ before do
186
+ subject.get('/declared') { declared(params, include_missing: true) }
187
+ end
188
+
189
+ it 'sets objects with type=Hash to be a hash' do
190
+ get '/declared?first=present'
191
+ expect(last_response.status).to eq(200)
192
+
193
+ body = JSON.parse(last_response.body)
194
+ expect(body['empty_hash']).to eq({})
195
+ expect(body['nested']).to be_a(Hash)
196
+ expect(body['nested']['empty_hash']).to eq({})
197
+ expect(body['nested']['nested_two']).to be_a(Hash)
198
+ end
199
+
200
+ it 'sets objects with type=Set to be a set' do
201
+ get '/declared?first=present'
202
+ expect(last_response.status).to eq(200)
203
+
204
+ body = JSON.parse(last_response.body)
205
+ expect(['#<Set: {}>', []]).to include(body['empty_set'])
206
+ expect(['#<Set: {}>', []]).to include(body['empty_typed_set'])
207
+ expect(['#<Set: {}>', []]).to include(body['nested']['empty_set'])
208
+ expect(['#<Set: {}>', []]).to include(body['nested']['empty_typed_set'])
209
+ end
210
+
211
+ it 'sets objects with type=Array to be an array' do
212
+ get '/declared?first=present'
213
+ expect(last_response.status).to eq(200)
214
+
215
+ body = JSON.parse(last_response.body)
216
+ expect(body['empty_arr']).to eq([])
217
+ expect(body['empty_typed_arr']).to eq([])
218
+ expect(body['arr']).to eq([])
219
+ expect(body['nested']['empty_arr']).to eq([])
220
+ expect(body['nested']['empty_typed_arr']).to eq([])
221
+ expect(body['nested']['nested_arr']).to eq([])
222
+ end
223
+
224
+ it 'includes all declared children when type=Hash' do
225
+ get '/declared?first=present'
226
+ expect(last_response.status).to eq(200)
227
+
228
+ body = JSON.parse(last_response.body)
229
+ expect(body['nested'].keys).to eq(%w[fourth fifth nested_two nested_arr empty_arr empty_typed_arr empty_hash empty_set empty_typed_set])
230
+ expect(body['nested']['nested_two'].keys).to eq(%w[sixth nested_three])
231
+ expect(body['nested']['nested_two']['nested_three'].keys).to eq(%w[seventh])
232
+ end
233
+ end
234
+
235
+ it 'filters out any additional params that are given' do
236
+ subject.get '/declared' do
237
+ declared(params)
238
+ end
239
+ get '/declared?first=one&other=two'
240
+ expect(last_response.status).to eq(200)
241
+ expect(JSON.parse(last_response.body).key?(:other)).to eq false
242
+ end
243
+
244
+ it 'stringifies if that option is passed' do
245
+ subject.get '/declared' do
246
+ declared(params, stringify: true)
247
+ end
248
+
249
+ get '/declared?first=one&other=two'
250
+ expect(last_response.status).to eq(200)
251
+ expect(JSON.parse(last_response.body)['first']).to eq 'one'
252
+ end
253
+
254
+ it 'does not include missing attributes if that option is passed' do
255
+ subject.get '/declared' do
256
+ error! 'expected nil', 400 if declared(params, include_missing: false).key?(:second)
257
+ ''
258
+ end
259
+
260
+ get '/declared?first=one&other=two'
261
+ expect(last_response.status).to eq(200)
262
+ end
263
+
264
+ it 'does not include renamed missing attributes if that option is passed' do
265
+ subject.params do
266
+ optional :renamed_original, as: :renamed
267
+ end
268
+ subject.get '/declared' do
269
+ error! 'expected nil', 400 if declared(params, include_missing: false).key?(:renamed)
270
+ ''
271
+ end
272
+
273
+ get '/declared?first=one&other=two'
274
+ expect(last_response.status).to eq(200)
275
+ end
276
+
277
+ it 'includes attributes with value that evaluates to false' do
278
+ subject.params do
279
+ requires :first
280
+ optional :boolean
281
+ end
282
+
283
+ subject.post '/declared' do
284
+ error!('expected false', 400) if declared(params, include_missing: false)[:boolean] != false
285
+ ''
286
+ end
287
+
288
+ post '/declared', ::Grape::Json.dump(first: 'one', boolean: false), 'CONTENT_TYPE' => 'application/json'
289
+ expect(last_response.status).to eq(201)
290
+ end
291
+
292
+ it 'includes attributes with value that evaluates to nil' do
293
+ subject.params do
294
+ requires :first
295
+ optional :second
296
+ end
297
+
298
+ subject.post '/declared' do
299
+ error!('expected nil', 400) unless declared(params, include_missing: false)[:second].nil?
300
+ ''
301
+ end
302
+
303
+ post '/declared', ::Grape::Json.dump(first: 'one', second: nil), 'CONTENT_TYPE' => 'application/json'
304
+ expect(last_response.status).to eq(201)
305
+ end
306
+
307
+ it 'includes missing attributes with defaults when there are nested hashes' do
308
+ subject.get '/dummy' do
309
+ end
310
+
311
+ subject.params do
312
+ requires :first
313
+ optional :second
314
+ optional :third, default: nil
315
+ optional :nested, type: Hash do
316
+ optional :fourth, default: nil
317
+ optional :fifth, default: nil
318
+ requires :nested_nested, type: Hash do
319
+ optional :sixth, default: 'sixth-default'
320
+ optional :seven, default: nil
321
+ end
322
+ end
323
+ end
324
+
325
+ subject.get '/declared' do
326
+ declared(params, include_missing: false)
327
+ end
328
+
329
+ get '/declared?first=present&nested[fourth]=&nested[nested_nested][sixth]=sixth'
330
+ json = JSON.parse(last_response.body)
331
+ expect(last_response.status).to eq(200)
332
+ expect(json['first']).to eq 'present'
333
+ expect(json['nested'].keys).to eq %w[fourth fifth nested_nested]
334
+ expect(json['nested']['fourth']).to eq ''
335
+ expect(json['nested']['nested_nested'].keys).to eq %w[sixth seven]
336
+ expect(json['nested']['nested_nested']['sixth']).to eq 'sixth'
337
+ end
338
+
339
+ it 'does not include missing attributes when there are nested hashes' do
340
+ subject.get '/dummy' do
341
+ end
342
+
343
+ subject.params do
344
+ requires :first
345
+ optional :second
346
+ optional :third
347
+ optional :nested, type: Hash do
348
+ optional :fourth
349
+ optional :fifth
350
+ end
351
+ end
352
+
353
+ subject.get '/declared' do
354
+ declared(params, include_missing: false)
355
+ end
356
+
357
+ get '/declared?first=present&nested[fourth]=4'
358
+ json = JSON.parse(last_response.body)
359
+ expect(last_response.status).to eq(200)
360
+ expect(json['first']).to eq 'present'
361
+ expect(json['nested'].keys).to eq %w[fourth]
362
+ expect(json['nested']['fourth']).to eq '4'
363
+ end
364
+ end
365
+
366
+ describe '#declared; call from child namespace' do
367
+ before do
368
+ subject.format :json
369
+ subject.namespace :parent do
370
+ params do
371
+ requires :parent_name, type: String
372
+ end
373
+
374
+ namespace ':parent_name' do
375
+ params do
376
+ requires :child_name, type: String
377
+ requires :child_age, type: Integer
378
+ end
379
+
380
+ namespace ':child_name' do
381
+ params do
382
+ requires :grandchild_name, type: String
383
+ end
384
+
385
+ get ':grandchild_name' do
386
+ {
387
+ 'params' => params,
388
+ 'without_parent_namespaces' => declared(params, include_parent_namespaces: false),
389
+ 'with_parent_namespaces' => declared(params, include_parent_namespaces: true)
390
+ }
391
+ end
392
+ end
393
+ end
394
+ end
395
+
396
+ get '/parent/foo/bar/baz', child_age: 5, extra: 'hello'
397
+ end
398
+
399
+ let(:parsed_response) { JSON.parse(last_response.body, symbolize_names: true) }
400
+
401
+ it { expect(last_response.status).to eq 200 }
402
+
403
+ context 'with include_parent_namespaces: false' do
404
+ it 'returns declared parameters only from current namespace' do
405
+ expect(parsed_response[:without_parent_namespaces]).to eq(
406
+ grandchild_name: 'baz'
407
+ )
408
+ end
409
+ end
410
+
411
+ context 'with include_parent_namespaces: true' do
412
+ it 'returns declared parameters from every parent namespace' do
413
+ expect(parsed_response[:with_parent_namespaces]).to eq(
414
+ parent_name: 'foo',
415
+ child_name: 'bar',
416
+ grandchild_name: 'baz',
417
+ child_age: 5
418
+ )
419
+ end
420
+ end
421
+
422
+ context 'without declaration' do
423
+ it 'returns all requested parameters' do
424
+ expect(parsed_response[:params]).to eq(
425
+ parent_name: 'foo',
426
+ child_name: 'bar',
427
+ grandchild_name: 'baz',
428
+ child_age: 5,
429
+ extra: 'hello'
430
+ )
431
+ end
432
+ end
433
+ end
434
+
435
+ describe '#declared; from a nested mounted endpoint' do
436
+ before do
437
+ doubly_mounted = Class.new(Grape::API)
438
+ doubly_mounted.namespace :more do
439
+ params do
440
+ requires :y, type: Integer
441
+ end
442
+ route_param :y do
443
+ get do
444
+ {
445
+ params: params,
446
+ declared_params: declared(params)
447
+ }
448
+ end
449
+ end
450
+ end
451
+
452
+ mounted = Class.new(Grape::API)
453
+ mounted.namespace :another do
454
+ params do
455
+ requires :mount_space, type: Integer
456
+ end
457
+ route_param :mount_space do
458
+ mount doubly_mounted
459
+ end
460
+ end
461
+
462
+ subject.format :json
463
+ subject.namespace :something do
464
+ params do
465
+ requires :id, type: Integer
466
+ end
467
+ resource ':id' do
468
+ mount mounted
469
+ end
470
+ end
471
+ end
472
+
473
+ it 'can access parent attributes' do
474
+ get '/something/123/another/456/more/789'
475
+ expect(last_response.status).to eq 200
476
+ json = JSON.parse(last_response.body, symbolize_names: true)
477
+
478
+ # test all three levels of params
479
+ expect(json[:declared_params][:y]).to eq 789
480
+ expect(json[:declared_params][:mount_space]).to eq 456
481
+ expect(json[:declared_params][:id]).to eq 123
482
+ end
483
+ end
484
+
485
+ describe '#declared; mixed nesting' do
486
+ before do
487
+ subject.format :json
488
+ subject.resource :users do
489
+ route_param :id, type: Integer, desc: 'ID desc' do
490
+ # Adding this causes route_setting(:declared_params) to be nil for the
491
+ # get block in namespace 'foo' below
492
+ get do
493
+ end
494
+
495
+ namespace 'foo' do
496
+ get do
497
+ {
498
+ params: params,
499
+ declared_params: declared(params),
500
+ declared_params_no_parent: declared(params, include_parent_namespaces: false)
501
+ }
502
+ end
503
+ end
504
+ end
505
+ end
506
+ end
507
+
508
+ it 'can access parent route_param' do
509
+ get '/users/123/foo', bar: 'bar'
510
+ expect(last_response.status).to eq 200
511
+ json = JSON.parse(last_response.body, symbolize_names: true)
512
+
513
+ expect(json[:declared_params][:id]).to eq 123
514
+ expect(json[:declared_params_no_parent][:id]).to eq nil
515
+ end
516
+ end
517
+
518
+ describe '#declared; with multiple route_param' do
519
+ before do
520
+ mounted = Class.new(Grape::API)
521
+ mounted.namespace :albums do
522
+ get do
523
+ declared(params)
524
+ end
525
+ end
526
+
527
+ subject.format :json
528
+ subject.namespace :artists do
529
+ route_param :id, type: Integer do
530
+ get do
531
+ declared(params)
532
+ end
533
+
534
+ params do
535
+ requires :filter, type: String
536
+ end
537
+ get :some_route do
538
+ declared(params)
539
+ end
540
+ end
541
+
542
+ route_param :artist_id, type: Integer do
543
+ namespace :compositions do
544
+ get do
545
+ declared(params)
546
+ end
547
+ end
548
+ end
549
+
550
+ route_param :compositor_id, type: Integer do
551
+ mount mounted
552
+ end
553
+ end
554
+ end
555
+
556
+ it 'return only :id without :artist_id' do
557
+ get '/artists/1'
558
+ json = JSON.parse(last_response.body, symbolize_names: true)
559
+
560
+ expect(json.key?(:id)).to be_truthy
561
+ expect(json.key?(:artist_id)).not_to be_truthy
562
+ end
563
+
564
+ it 'return only :artist_id without :id' do
565
+ get '/artists/1/compositions'
566
+ json = JSON.parse(last_response.body, symbolize_names: true)
567
+
568
+ expect(json.key?(:artist_id)).to be_truthy
569
+ expect(json.key?(:id)).not_to be_truthy
570
+ end
571
+
572
+ it 'return :filter and :id parameters in declared for second enpoint inside route_param' do
573
+ get '/artists/1/some_route', filter: 'some_filter'
574
+ json = JSON.parse(last_response.body, symbolize_names: true)
575
+
576
+ expect(json.key?(:filter)).to be_truthy
577
+ expect(json.key?(:id)).to be_truthy
578
+ expect(json.key?(:artist_id)).not_to be_truthy
579
+ end
580
+
581
+ it 'return :compositor_id for mounter in route_param' do
582
+ get '/artists/1/albums'
583
+ json = JSON.parse(last_response.body, symbolize_names: true)
584
+
585
+ expect(json.key?(:compositor_id)).to be_truthy
586
+ expect(json.key?(:id)).not_to be_truthy
587
+ expect(json.key?(:artist_id)).not_to be_truthy
588
+ end
589
+ end
590
+ end