grape 1.3.2 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -2
  3. data/LICENSE +1 -1
  4. data/README.md +120 -24
  5. data/UPGRADING.md +220 -39
  6. data/lib/grape.rb +3 -2
  7. data/lib/grape/api.rb +3 -3
  8. data/lib/grape/api/instance.rb +22 -25
  9. data/lib/grape/dsl/callbacks.rb +1 -1
  10. data/lib/grape/dsl/helpers.rb +1 -0
  11. data/lib/grape/dsl/inside_route.rb +70 -37
  12. data/lib/grape/dsl/parameters.rb +8 -4
  13. data/lib/grape/dsl/routing.rb +6 -7
  14. data/lib/grape/dsl/validations.rb +18 -1
  15. data/lib/grape/eager_load.rb +1 -1
  16. data/lib/grape/endpoint.rb +8 -6
  17. data/lib/grape/exceptions/validation.rb +1 -1
  18. data/lib/grape/exceptions/validation_errors.rb +1 -1
  19. data/lib/grape/middleware/auth/base.rb +3 -3
  20. data/lib/grape/middleware/base.rb +3 -2
  21. data/lib/grape/middleware/error.rb +11 -13
  22. data/lib/grape/middleware/formatter.rb +3 -3
  23. data/lib/grape/middleware/stack.rb +8 -1
  24. data/lib/grape/request.rb +1 -1
  25. data/lib/grape/router.rb +25 -39
  26. data/lib/grape/router/attribute_translator.rb +26 -5
  27. data/lib/grape/router/route.rb +1 -19
  28. data/lib/grape/{serve_file → serve_stream}/file_body.rb +1 -1
  29. data/lib/grape/{serve_file → serve_stream}/sendfile_response.rb +1 -1
  30. data/lib/grape/{serve_file/file_response.rb → serve_stream/stream_response.rb} +8 -8
  31. data/lib/grape/util/base_inheritable.rb +2 -2
  32. data/lib/grape/util/lazy_value.rb +1 -0
  33. data/lib/grape/validations/attributes_iterator.rb +8 -0
  34. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  35. data/lib/grape/validations/params_scope.rb +9 -7
  36. data/lib/grape/validations/single_attribute_iterator.rb +1 -1
  37. data/lib/grape/validations/types.rb +1 -4
  38. data/lib/grape/validations/types/array_coercer.rb +14 -5
  39. data/lib/grape/validations/types/build_coercer.rb +1 -5
  40. data/lib/grape/validations/types/custom_type_coercer.rb +15 -1
  41. data/lib/grape/validations/types/dry_type_coercer.rb +36 -1
  42. data/lib/grape/validations/types/invalid_value.rb +24 -0
  43. data/lib/grape/validations/types/primitive_coercer.rb +9 -3
  44. data/lib/grape/validations/types/set_coercer.rb +6 -4
  45. data/lib/grape/validations/types/variant_collection_coercer.rb +1 -1
  46. data/lib/grape/validations/validator_factory.rb +1 -1
  47. data/lib/grape/validations/validators/as.rb +1 -1
  48. data/lib/grape/validations/validators/base.rb +8 -8
  49. data/lib/grape/validations/validators/coerce.rb +8 -14
  50. data/lib/grape/validations/validators/default.rb +3 -5
  51. data/lib/grape/validations/validators/except_values.rb +1 -1
  52. data/lib/grape/validations/validators/multiple_params_base.rb +2 -1
  53. data/lib/grape/validations/validators/values.rb +1 -1
  54. data/lib/grape/version.rb +1 -1
  55. data/spec/grape/api/instance_spec.rb +50 -0
  56. data/spec/grape/api_remount_spec.rb +9 -4
  57. data/spec/grape/api_spec.rb +75 -0
  58. data/spec/grape/dsl/inside_route_spec.rb +182 -33
  59. data/spec/grape/endpoint/declared_spec.rb +601 -0
  60. data/spec/grape/endpoint_spec.rb +0 -521
  61. data/spec/grape/entity_spec.rb +7 -1
  62. data/spec/grape/integration/rack_sendfile_spec.rb +12 -8
  63. data/spec/grape/middleware/auth/strategies_spec.rb +1 -1
  64. data/spec/grape/middleware/error_spec.rb +1 -1
  65. data/spec/grape/middleware/formatter_spec.rb +1 -1
  66. data/spec/grape/middleware/stack_spec.rb +1 -0
  67. data/spec/grape/request_spec.rb +1 -1
  68. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +13 -3
  69. data/spec/grape/validations/params_scope_spec.rb +26 -0
  70. data/spec/grape/validations/single_attribute_iterator_spec.rb +17 -6
  71. data/spec/grape/validations/types/array_coercer_spec.rb +35 -0
  72. data/spec/grape/validations/types/primitive_coercer_spec.rb +65 -5
  73. data/spec/grape/validations/types/set_coercer_spec.rb +34 -0
  74. data/spec/grape/validations/validators/coerce_spec.rb +223 -25
  75. data/spec/grape/validations/validators/default_spec.rb +170 -0
  76. data/spec/grape/validations/validators/except_values_spec.rb +1 -0
  77. data/spec/grape/validations/validators/values_spec.rb +1 -1
  78. data/spec/grape/validations_spec.rb +290 -18
  79. data/spec/integration/eager_load/eager_load_spec.rb +15 -0
  80. data/spec/shared/versioning_examples.rb +20 -20
  81. data/spec/spec_helper.rb +0 -10
  82. data/spec/support/chunks.rb +14 -0
  83. data/spec/support/versioned_helpers.rb +4 -6
  84. metadata +20 -9
@@ -3,13 +3,12 @@
3
3
  module Grape
4
4
  module Validations
5
5
  class DefaultValidator < Base
6
- def initialize(attrs, options, required, scope, opts = {})
6
+ def initialize(attrs, options, required, scope, **opts)
7
7
  @default = options
8
8
  super
9
9
  end
10
10
 
11
11
  def validate_param!(attr_name, params)
12
- return if params.key? attr_name
13
12
  params[attr_name] = if @default.is_a? Proc
14
13
  @default.call
15
14
  elsif @default.frozen? || !duplicatable?(@default)
@@ -22,9 +21,8 @@ module Grape
22
21
  def validate!(params)
23
22
  attrs = SingleAttributeIterator.new(self, @scope, params)
24
23
  attrs.each do |resource_params, attr_name|
25
- if resource_params.is_a?(Hash) && resource_params[attr_name].nil?
26
- validate_param!(attr_name, resource_params)
27
- end
24
+ next unless @scope.meets_dependency?(resource_params, params)
25
+ validate_param!(attr_name, resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil?
28
26
  end
29
27
  end
30
28
 
@@ -3,7 +3,7 @@
3
3
  module Grape
4
4
  module Validations
5
5
  class ExceptValuesValidator < Base
6
- def initialize(attrs, options, required, scope, opts = {})
6
+ def initialize(attrs, options, required, scope, **opts)
7
7
  @except = options.is_a?(Hash) ? options[:value] : options
8
8
  super
9
9
  end
@@ -7,7 +7,8 @@ module Grape
7
7
  attributes = MultipleAttributesIterator.new(self, @scope, params)
8
8
  array_errors = []
9
9
 
10
- attributes.each do |resource_params|
10
+ attributes.each do |resource_params, skip_value|
11
+ next if skip_value
11
12
  begin
12
13
  validate_params!(resource_params)
13
14
  rescue Grape::Exceptions::Validation => e
@@ -3,7 +3,7 @@
3
3
  module Grape
4
4
  module Validations
5
5
  class ValuesValidator < Base
6
- def initialize(attrs, options, required, scope, opts = {})
6
+ def initialize(attrs, options, required, scope, **opts)
7
7
  if options.is_a?(Hash)
8
8
  @excepts = options[:except]
9
9
  @values = options[:value]
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.2'
5
+ VERSION = '1.5.2'
6
6
  end
@@ -51,4 +51,54 @@ describe Grape::API::Instance do
51
51
  expect(an_instance.top_level_setting.parent).to be_nil
52
52
  end
53
53
  end
54
+
55
+ context 'with multiple moutes' do
56
+ let(:first) do
57
+ Class.new(Grape::API::Instance) do
58
+ namespace(:some_namespace) do
59
+ route :any, '*path' do
60
+ error!('Not found! (1)', 404)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ let(:second) do
66
+ Class.new(Grape::API::Instance) do
67
+ namespace(:another_namespace) do
68
+ route :any, '*path' do
69
+ error!('Not found! (2)', 404)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ let(:root_api) do
75
+ first_instance = first
76
+ second_instance = second
77
+ Class.new(Grape::API) do
78
+ mount first_instance
79
+ mount first_instance
80
+ mount second_instance
81
+ end
82
+ end
83
+
84
+ it 'does not raise a FrozenError on first instance' do
85
+ expect { patch '/some_namespace/anything' }.not_to \
86
+ raise_error
87
+ end
88
+
89
+ it 'responds the correct body at the first instance' do
90
+ patch '/some_namespace/anything'
91
+ expect(last_response.body).to eq 'Not found! (1)'
92
+ end
93
+
94
+ it 'does not raise a FrozenError on second instance' do
95
+ expect { get '/another_namespace/other' }.not_to \
96
+ raise_error
97
+ end
98
+
99
+ it 'responds the correct body at the second instance' do
100
+ get '/another_namespace/foobar'
101
+ expect(last_response.body).to eq 'Not found! (2)'
102
+ end
103
+ end
54
104
  end
@@ -340,19 +340,24 @@ describe Grape::API do
340
340
  context 'when the configuration is read within a namespace' do
341
341
  before do
342
342
  a_remounted_api.namespace 'api' do
343
+ params do
344
+ requires configuration[:required_param]
345
+ end
343
346
  get "/#{configuration[:path]}" do
344
347
  '10 votes'
345
348
  end
346
349
  end
347
- root_api.mount a_remounted_api, with: { path: 'votes' }
348
- root_api.mount a_remounted_api, with: { path: 'scores' }
350
+ root_api.mount a_remounted_api, with: { path: 'votes', required_param: 'param_key' }
351
+ root_api.mount a_remounted_api, with: { path: 'scores', required_param: 'param_key' }
349
352
  end
350
353
 
351
354
  it 'will use the dynamic configuration on all routes' do
352
- get 'api/votes'
355
+ get 'api/votes', param_key: 'a'
353
356
  expect(last_response.body).to eql '10 votes'
354
- get 'api/scores'
357
+ get 'api/scores', param_key: 'a'
355
358
  expect(last_response.body).to eql '10 votes'
359
+ get 'api/votes'
360
+ expect(last_response.status).to eq 400
356
361
  end
357
362
  end
358
363
 
@@ -816,6 +816,71 @@ XML
816
816
  end
817
817
  end
818
818
 
819
+ describe 'when hook behaviour is controlled by attributes on the route ' do
820
+ before do
821
+ subject.before do
822
+ error!('Access Denied', 401) unless route.options[:secret] == params[:secret]
823
+ end
824
+
825
+ subject.namespace 'example' do
826
+ before do
827
+ error!('Access Denied', 401) unless route.options[:namespace_secret] == params[:namespace_secret]
828
+ end
829
+
830
+ desc 'it gets with secret', secret: 'password'
831
+ get { status(params[:id] == '504' ? 200 : 404) }
832
+
833
+ desc 'it post with secret', secret: 'password', namespace_secret: 'namespace_password'
834
+ post {}
835
+ end
836
+ end
837
+
838
+ context 'when HTTP method is not defined' do
839
+ let(:response) { delete('/example') }
840
+
841
+ it 'responds with a 405 status' do
842
+ expect(response.status).to eql 405
843
+ end
844
+ end
845
+
846
+ context 'when HTTP method is defined with attribute' do
847
+ let(:response) { post('/example?secret=incorrect_password') }
848
+ it 'responds with the defined error in the before hook' do
849
+ expect(response.status).to eql 401
850
+ end
851
+ end
852
+
853
+ context 'when HTTP method is defined and the underlying before hook expectation is not met' do
854
+ let(:response) { post('/example?secret=password&namespace_secret=wrong_namespace_password') }
855
+ it 'ends up in the endpoint' do
856
+ expect(response.status).to eql 401
857
+ end
858
+ end
859
+
860
+ context 'when HTTP method is defined and everything is like the before hooks expect' do
861
+ let(:response) { post('/example?secret=password&namespace_secret=namespace_password') }
862
+ it 'ends up in the endpoint' do
863
+ expect(response.status).to eql 201
864
+ end
865
+ end
866
+
867
+ context 'when HEAD is called for the defined GET' do
868
+ let(:response) { head('/example?id=504') }
869
+
870
+ it 'responds with 401 because before expectations in before hooks are not met' do
871
+ expect(response.status).to eql 401
872
+ end
873
+ end
874
+
875
+ context 'when HEAD is called for the defined GET' do
876
+ let(:response) { head('/example?id=504&secret=password') }
877
+
878
+ it 'responds with 200 because before hooks are not called' do
879
+ expect(response.status).to eql 200
880
+ end
881
+ end
882
+ end
883
+
819
884
  context 'allows HEAD on a GET request that' do
820
885
  before do
821
886
  subject.get 'example' do
@@ -1084,6 +1149,11 @@ XML
1084
1149
  expect(last_response.headers['Content-Type']).to eq('text/plain')
1085
1150
  end
1086
1151
 
1152
+ it 'does not set Cache-Control' do
1153
+ get '/foo'
1154
+ expect(last_response.headers['Cache-Control']).to eq(nil)
1155
+ end
1156
+
1087
1157
  it 'sets content type for xml' do
1088
1158
  get '/foo.xml'
1089
1159
  expect(last_response.headers['Content-Type']).to eq('application/xml')
@@ -1530,6 +1600,11 @@ XML
1530
1600
  expect(subject.io).to receive(:write).with(message)
1531
1601
  subject.logger.info 'this will be logged'
1532
1602
  end
1603
+
1604
+ it 'does not unnecessarily retain duplicate setup blocks' do
1605
+ subject.logger
1606
+ expect { subject.logger }.to_not change(subject.instance_variable_get(:@setup), :size)
1607
+ end
1533
1608
  end
1534
1609
 
1535
1610
  describe '.helpers' do
@@ -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