grape 1.3.0 → 1.5.0

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 (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