grape 1.3.1 → 1.5.1

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -1
  3. data/LICENSE +1 -1
  4. data/README.md +104 -21
  5. data/UPGRADING.md +265 -39
  6. data/lib/grape.rb +2 -2
  7. data/lib/grape/api.rb +2 -2
  8. data/lib/grape/api/instance.rb +29 -28
  9. data/lib/grape/dsl/helpers.rb +1 -0
  10. data/lib/grape/dsl/inside_route.rb +69 -36
  11. data/lib/grape/dsl/parameters.rb +7 -3
  12. data/lib/grape/dsl/routing.rb +2 -4
  13. data/lib/grape/dsl/validations.rb +18 -1
  14. data/lib/grape/eager_load.rb +1 -1
  15. data/lib/grape/endpoint.rb +8 -6
  16. data/lib/grape/http/headers.rb +1 -0
  17. data/lib/grape/middleware/base.rb +2 -1
  18. data/lib/grape/middleware/error.rb +10 -12
  19. data/lib/grape/middleware/formatter.rb +3 -3
  20. data/lib/grape/middleware/stack.rb +21 -8
  21. data/lib/grape/middleware/versioner/header.rb +1 -1
  22. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
  23. data/lib/grape/path.rb +2 -2
  24. data/lib/grape/request.rb +1 -1
  25. data/lib/grape/router.rb +30 -43
  26. data/lib/grape/router/attribute_translator.rb +26 -5
  27. data/lib/grape/router/route.rb +3 -22
  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 +11 -8
  32. data/lib/grape/util/lazy_value.rb +1 -0
  33. data/lib/grape/util/reverse_stackable_values.rb +3 -1
  34. data/lib/grape/util/stackable_values.rb +3 -1
  35. data/lib/grape/validations/attributes_iterator.rb +8 -0
  36. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  37. data/lib/grape/validations/params_scope.rb +8 -6
  38. data/lib/grape/validations/single_attribute_iterator.rb +1 -1
  39. data/lib/grape/validations/types.rb +6 -5
  40. data/lib/grape/validations/types/array_coercer.rb +14 -5
  41. data/lib/grape/validations/types/build_coercer.rb +5 -8
  42. data/lib/grape/validations/types/custom_type_coercer.rb +14 -2
  43. data/lib/grape/validations/types/dry_type_coercer.rb +36 -1
  44. data/lib/grape/validations/types/file.rb +15 -13
  45. data/lib/grape/validations/types/json.rb +40 -36
  46. data/lib/grape/validations/types/primitive_coercer.rb +11 -5
  47. data/lib/grape/validations/types/set_coercer.rb +6 -4
  48. data/lib/grape/validations/types/variant_collection_coercer.rb +1 -1
  49. data/lib/grape/validations/validator_factory.rb +1 -1
  50. data/lib/grape/validations/validators/as.rb +1 -1
  51. data/lib/grape/validations/validators/base.rb +4 -5
  52. data/lib/grape/validations/validators/coerce.rb +3 -10
  53. data/lib/grape/validations/validators/default.rb +3 -5
  54. data/lib/grape/validations/validators/except_values.rb +1 -1
  55. data/lib/grape/validations/validators/multiple_params_base.rb +2 -1
  56. data/lib/grape/validations/validators/regexp.rb +1 -1
  57. data/lib/grape/validations/validators/values.rb +1 -1
  58. data/lib/grape/version.rb +1 -1
  59. data/spec/grape/api/instance_spec.rb +50 -0
  60. data/spec/grape/api_spec.rb +75 -0
  61. data/spec/grape/dsl/inside_route_spec.rb +182 -33
  62. data/spec/grape/endpoint/declared_spec.rb +601 -0
  63. data/spec/grape/endpoint_spec.rb +0 -521
  64. data/spec/grape/entity_spec.rb +6 -0
  65. data/spec/grape/integration/rack_sendfile_spec.rb +12 -8
  66. data/spec/grape/middleware/auth/strategies_spec.rb +1 -1
  67. data/spec/grape/middleware/error_spec.rb +1 -1
  68. data/spec/grape/middleware/formatter_spec.rb +1 -1
  69. data/spec/grape/middleware/stack_spec.rb +3 -1
  70. data/spec/grape/path_spec.rb +4 -4
  71. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +13 -3
  72. data/spec/grape/validations/params_scope_spec.rb +26 -0
  73. data/spec/grape/validations/single_attribute_iterator_spec.rb +17 -6
  74. data/spec/grape/validations/types/array_coercer_spec.rb +35 -0
  75. data/spec/grape/validations/types/primitive_coercer_spec.rb +65 -5
  76. data/spec/grape/validations/types/set_coercer_spec.rb +34 -0
  77. data/spec/grape/validations/types_spec.rb +1 -1
  78. data/spec/grape/validations/validators/coerce_spec.rb +317 -29
  79. data/spec/grape/validations/validators/default_spec.rb +170 -0
  80. data/spec/grape/validations/validators/except_values_spec.rb +1 -0
  81. data/spec/grape/validations/validators/values_spec.rb +1 -1
  82. data/spec/grape/validations_spec.rb +290 -18
  83. data/spec/integration/eager_load/eager_load_spec.rb +15 -0
  84. data/spec/spec_helper.rb +0 -10
  85. data/spec/support/chunks.rb +14 -0
  86. data/spec/support/versioned_helpers.rb +3 -5
  87. metadata +18 -8
@@ -11,10 +11,10 @@ module Grape
11
11
  class PrimitiveCoercer < DryTypeCoercer
12
12
  MAPPING = {
13
13
  Grape::API::Boolean => DryTypes::Params::Bool,
14
+ BigDecimal => DryTypes::Params::Decimal,
14
15
 
15
16
  # unfortunately, a +Params+ scope doesn't contain String
16
- String => DryTypes::Coercible::String,
17
- BigDecimal => DryTypes::Coercible::Decimal
17
+ String => DryTypes::Coercible::String
18
18
  }.freeze
19
19
 
20
20
  STRICT_MAPPING = {
@@ -36,8 +36,7 @@ module Grape
36
36
 
37
37
  def call(val)
38
38
  return InvalidValue.new if reject?(val)
39
- return nil if val.nil?
40
- return '' if val == ''
39
+ return nil if val.nil? || treat_as_nil?(val)
41
40
 
42
41
  super
43
42
  end
@@ -46,7 +45,7 @@ module Grape
46
45
 
47
46
  attr_reader :type
48
47
 
49
- # This method maintaine logic which was defined by Virtus. For example,
48
+ # This method maintains logic which was defined by Virtus. For example,
50
49
  # dry-types is ok to convert an array or a hash to a string, it is supported,
51
50
  # but Virtus wouldn't accept it. So, this method only exists to not introduce
52
51
  # breaking changes.
@@ -55,6 +54,13 @@ module Grape
55
54
  (val.is_a?(String) && type == Hash) ||
56
55
  (val.is_a?(Hash) && type == String)
57
56
  end
57
+
58
+ # Dry-Types treats an empty string as invalid. However, Grape considers an empty string as
59
+ # absence of a value and coerces it into nil. See a discussion there
60
+ # https://github.com/ruby-grape/grape/pull/2045
61
+ def treat_as_nil?(val)
62
+ val == '' && type != String
63
+ end
58
64
  end
59
65
  end
60
66
  end
@@ -1,18 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'set'
4
- require_relative 'dry_type_coercer'
4
+ require_relative 'array_coercer'
5
5
 
6
6
  module Grape
7
7
  module Validations
8
8
  module Types
9
9
  # Takes the given array and converts it to a set. Every element of the set
10
10
  # is also coerced.
11
- class SetCoercer < DryTypeCoercer
11
+ class SetCoercer < ArrayCoercer
12
+ register_collection Set
13
+
12
14
  def initialize(type, strict = false)
13
15
  super
14
16
 
15
- @elem_coercer = PrimitiveCoercer.new(type.first, strict)
17
+ @coercer = nil
16
18
  end
17
19
 
18
20
  def call(value)
@@ -25,7 +27,7 @@ module Grape
25
27
 
26
28
  def coerce_elements(collection)
27
29
  collection.each_with_object(Set.new) do |elem, memo|
28
- coerced_elem = @elem_coercer.call(elem)
30
+ coerced_elem = elem_coercer.call(elem)
29
31
 
30
32
  return coerced_elem if coerced_elem.is_a?(InvalidValue)
31
33
 
@@ -33,7 +33,7 @@ module Grape
33
33
  # the coerced result, or an instance
34
34
  # of {InvalidValue} if the value could not be coerced.
35
35
  def call(value)
36
- return InvalidValue.new unless value.is_a? Array
36
+ return unless value.is_a? Array
37
37
 
38
38
  value =
39
39
  if @method
@@ -8,7 +8,7 @@ module Grape
8
8
  options[:options],
9
9
  options[:required],
10
10
  options[:params_scope],
11
- options[:opts])
11
+ **options[:opts])
12
12
  end
13
13
  end
14
14
  end
@@ -3,7 +3,7 @@
3
3
  module Grape
4
4
  module Validations
5
5
  class AsValidator < Base
6
- def initialize(attrs, options, required, scope, opts = {})
6
+ def initialize(attrs, options, required, scope, **opts)
7
7
  @renamed_options = options
8
8
  super
9
9
  end
@@ -13,7 +13,7 @@ module Grape
13
13
  # @param required [Boolean] attribute(s) are required or optional
14
14
  # @param scope [ParamsScope] parent scope for this Validator
15
15
  # @param opts [Hash] additional validation options
16
- def initialize(attrs, options, required, scope, opts = {})
16
+ def initialize(attrs, options, required, scope, **opts)
17
17
  @attrs = Array(attrs)
18
18
  @option = options
19
19
  @required = required
@@ -43,13 +43,12 @@ module Grape
43
43
  # there may be more than one error per field
44
44
  array_errors = []
45
45
 
46
- attributes.each do |val, attr_name, empty_val|
46
+ attributes.each do |val, attr_name, empty_val, skip_value|
47
+ next if skip_value
47
48
  next if !@scope.required? && empty_val
48
49
  next unless @scope.meets_dependency?(val, params)
49
50
  begin
50
- if @required || val.respond_to?(:key?) && val.key?(attr_name)
51
- validate_param!(attr_name, val)
52
- end
51
+ validate_param!(attr_name, val) if @required || val.respond_to?(:key?) && val.key?(attr_name)
53
52
  rescue Grape::Exceptions::Validation => e
54
53
  array_errors << e
55
54
  end
@@ -47,7 +47,9 @@ module Grape
47
47
  # h[:list] = list
48
48
  # h
49
49
  # => #<Hashie::Mash list=[1, 2, 3, 4]>
50
- params[attr_name] = new_value unless params[attr_name] == new_value
50
+ return if params[attr_name].class == new_value.class && params[attr_name] == new_value
51
+
52
+ params[attr_name] = new_value
51
53
  end
52
54
 
53
55
  private
@@ -65,16 +67,7 @@ module Grape
65
67
  end
66
68
 
67
69
  def coerce_value(val)
68
- # define default values for structures, the dry-types lib which is used
69
- # for coercion doesn't accept nil as a value, so it would fail
70
- if val.nil?
71
- return [] if type == Array || type.is_a?(Array)
72
- return Set.new if type == Set
73
- return {} if type == Hash
74
- end
75
-
76
70
  converter.call(val)
77
-
78
71
  # Some custom types might fail, so it should be treated as an invalid value
79
72
  rescue StandardError
80
73
  Types::InvalidValue.new
@@ -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
@@ -5,7 +5,7 @@ module Grape
5
5
  class RegexpValidator < Base
6
6
  def validate_param!(attr_name, params)
7
7
  return unless params.respond_to?(:key?) && params.key?(attr_name)
8
- return if Array.wrap(params[attr_name]).all? { |param| param.nil? || (param.to_s =~ (options_key?(:value) ? @option[:value] : @option)) }
8
+ return if Array.wrap(params[attr_name]).all? { |param| param.nil? || param.to_s.match?((options_key?(:value) ? @option[:value] : @option)) }
9
9
  raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:regexp))
10
10
  end
11
11
  end
@@ -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]
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Grape
4
4
  # The current version of Grape.
5
- VERSION = '1.3.1'
5
+ VERSION = '1.5.1'
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
@@ -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