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
@@ -206,12 +206,12 @@ module Grape
206
206
  end
207
207
  end
208
208
 
209
- module ServeFile
209
+ module ServeStream
210
210
  extend ::ActiveSupport::Autoload
211
211
  eager_autoload do
212
- autoload :FileResponse
213
212
  autoload :FileBody
214
213
  autoload :SendfileResponse
214
+ autoload :StreamResponse
215
215
  end
216
216
  end
217
217
  end
@@ -30,7 +30,7 @@ module Grape
30
30
  # an instance that will be used to create the set up but will not be mounted
31
31
  def initial_setup(base_instance_parent)
32
32
  @instances = []
33
- @setup = []
33
+ @setup = Set.new
34
34
  @base_parent = base_instance_parent
35
35
  @base_instance = mount_instance
36
36
  end
@@ -90,7 +90,7 @@ module Grape
90
90
  # For instance, a descripcion could be done using: `desc configuration[:description]` if it may vary
91
91
  # depending on where the endpoint is mounted. Use with care, if you find yourself using configuration
92
92
  # too much, you may actually want to provide a new API rather than remount it.
93
- def mount_instance(opts = {})
93
+ def mount_instance(**opts)
94
94
  instance = Class.new(@base_parent)
95
95
  instance.configuration = Grape::Util::EndpointConfiguration.new(opts[:configuration] || {})
96
96
  instance.base = self
@@ -192,36 +192,15 @@ module Grape
192
192
  # will return an HTTP 405 response for any HTTP method that the resource
193
193
  # cannot handle.
194
194
  def add_head_not_allowed_methods_and_options_methods
195
- routes_map = {}
196
-
197
- self.class.endpoints.each do |endpoint|
198
- routes = endpoint.routes
199
- routes.each do |route|
200
- # using the :any shorthand produces [nil] for route methods, substitute all manually
201
- route_key = route.pattern.to_regexp
202
- routes_map[route_key] ||= {}
203
- route_settings = routes_map[route_key]
204
- route_settings[:pattern] = route.pattern
205
- route_settings[:requirements] = route.requirements
206
- route_settings[:path] = route.origin
207
- route_settings[:methods] ||= []
208
- route_settings[:methods] << route.request_method
209
- route_settings[:endpoint] = route.app
210
-
211
- # using the :any shorthand produces [nil] for route methods, substitute all manually
212
- route_settings[:methods] = Grape::Http::Headers::SUPPORTED_METHODS if route_settings[:methods].include?('*')
213
- end
214
- end
215
-
195
+ versioned_route_configs = collect_route_config_per_pattern
216
196
  # The paths we collected are prepared (cf. Path#prepare), so they
217
197
  # contain already versioning information when using path versioning.
218
198
  # Disable versioning so adding a route won't prepend versioning
219
199
  # informations again.
220
200
  without_root_prefix do
221
201
  without_versioning do
222
- routes_map.each_value do |config|
223
- methods = config[:methods]
224
- allowed_methods = methods.dup
202
+ versioned_route_configs.each do |config|
203
+ allowed_methods = config[:methods].dup
225
204
 
226
205
  unless self.class.namespace_inheritable(:do_not_route_head)
227
206
  allowed_methods |= [Grape::Http::Headers::HEAD] if allowed_methods.include?(Grape::Http::Headers::GET)
@@ -240,14 +219,36 @@ module Grape
240
219
  end
241
220
  end
242
221
 
222
+ def collect_route_config_per_pattern
223
+ all_routes = self.class.endpoints.map(&:routes).flatten
224
+ routes_by_regexp = all_routes.group_by { |route| route.pattern.to_regexp }
225
+
226
+ # Build the configuration based on the first endpoint and the collection of methods supported.
227
+ routes_by_regexp.values.map do |routes|
228
+ last_route = routes.last # Most of the configuration is taken from the last endpoint
229
+ matching_wildchar = routes.any? { |route| route.request_method == '*' }
230
+ {
231
+ options: {},
232
+ pattern: last_route.pattern,
233
+ requirements: last_route.requirements,
234
+ path: last_route.origin,
235
+ endpoint: last_route.app,
236
+ methods: matching_wildchar ? Grape::Http::Headers::SUPPORTED_METHODS : routes.map(&:request_method)
237
+ }
238
+ end
239
+ end
240
+
243
241
  # Generate a route that returns an HTTP 405 response for a user defined
244
242
  # path on methods not specified
245
243
  def generate_not_allowed_method(pattern, allowed_methods: [], **attributes)
246
- not_allowed_methods = %w[GET PUT POST DELETE PATCH HEAD] - allowed_methods
247
- not_allowed_methods << Grape::Http::Headers::OPTIONS if self.class.namespace_inheritable(:do_not_route_options)
248
-
244
+ supported_methods =
245
+ if self.class.namespace_inheritable(:do_not_route_options)
246
+ Grape::Http::Headers::SUPPORTED_METHODS
247
+ else
248
+ Grape::Http::Headers::SUPPORTED_METHODS_WITHOUT_OPTIONS
249
+ end
250
+ not_allowed_methods = supported_methods - allowed_methods
249
251
  return if not_allowed_methods.empty?
250
-
251
252
  @router.associate_routes(pattern, not_allowed_methods: not_allowed_methods, **attributes)
252
253
  end
253
254
 
@@ -81,6 +81,7 @@ module Grape
81
81
  # to provide some API-specific functionality.
82
82
  module BaseHelper
83
83
  attr_accessor :api
84
+
84
85
  def params(name, &block)
85
86
  @named_params ||= {}
86
87
  @named_params[name] = block
@@ -28,36 +28,38 @@ module Grape
28
28
  # Methods which should not be available in filters until the before filter
29
29
  # has completed
30
30
  module PostBeforeFilter
31
- def declared(passed_params, options = {}, declared_params = nil)
31
+ def declared(passed_params, options = {}, declared_params = nil, params_nested_path = [])
32
32
  options = options.reverse_merge(include_missing: true, include_parent_namespaces: true)
33
33
  declared_params ||= optioned_declared_params(**options)
34
34
 
35
35
  if passed_params.is_a?(Array)
36
- declared_array(passed_params, options, declared_params)
36
+ declared_array(passed_params, options, declared_params, params_nested_path)
37
37
  else
38
- declared_hash(passed_params, options, declared_params)
38
+ declared_hash(passed_params, options, declared_params, params_nested_path)
39
39
  end
40
40
  end
41
41
 
42
42
  private
43
43
 
44
- def declared_array(passed_params, options, declared_params)
44
+ def declared_array(passed_params, options, declared_params, params_nested_path)
45
45
  passed_params.map do |passed_param|
46
- declared(passed_param || {}, options, declared_params)
46
+ declared(passed_param || {}, options, declared_params, params_nested_path)
47
47
  end
48
48
  end
49
49
 
50
- def declared_hash(passed_params, options, declared_params)
50
+ def declared_hash(passed_params, options, declared_params, params_nested_path)
51
51
  declared_params.each_with_object(passed_params.class.new) do |declared_param, memo|
52
52
  if declared_param.is_a?(Hash)
53
53
  declared_param.each_pair do |declared_parent_param, declared_children_params|
54
+ params_nested_path_dup = params_nested_path.dup
55
+ params_nested_path_dup << declared_parent_param.to_s
54
56
  next unless options[:include_missing] || passed_params.key?(declared_parent_param)
55
57
 
56
58
  passed_children_params = passed_params[declared_parent_param] || passed_params.class.new
57
59
  memo_key = optioned_param_key(declared_parent_param, options)
58
60
 
59
- memo[memo_key] = handle_passed_param(declared_parent_param, passed_children_params) do
60
- declared(passed_children_params, options, declared_children_params)
61
+ memo[memo_key] = handle_passed_param(params_nested_path_dup, passed_children_params.any?) do
62
+ declared(passed_children_params, options, declared_children_params, params_nested_path_dup)
61
63
  end
62
64
  end
63
65
  else
@@ -68,30 +70,37 @@ module Grape
68
70
 
69
71
  next unless options[:include_missing] || passed_params.key?(declared_param) || (param_renaming && passed_params.key?(param_renaming))
70
72
 
71
- if param_renaming
72
- memo[optioned_param_key(param_renaming, options)] = passed_params[param_renaming]
73
- else
74
- memo[optioned_param_key(declared_param, options)] = passed_params[declared_param]
73
+ memo_key = optioned_param_key(param_renaming || declared_param, options)
74
+ passed_param = passed_params[param_renaming || declared_param]
75
+
76
+ params_nested_path_dup = params_nested_path.dup
77
+ params_nested_path_dup << declared_param.to_s
78
+ memo[memo_key] = passed_param || handle_passed_param(params_nested_path_dup) do
79
+ passed_param
75
80
  end
76
81
  end
77
82
  end
78
83
  end
79
84
 
80
- def handle_passed_param(declared_param, passed_children_params, &_block)
81
- should_be_empty_array?(declared_param, passed_children_params) ? [] : yield
82
- end
85
+ def handle_passed_param(params_nested_path, has_passed_children = false, &_block)
86
+ return yield if has_passed_children
83
87
 
84
- def should_be_empty_array?(declared_param, passed_children_params)
85
- declared_param_is_array?(declared_param) && passed_children_params.empty?
86
- end
88
+ key = params_nested_path[0]
89
+ key += '[' + params_nested_path[1..-1].join('][') + ']' if params_nested_path.size > 1
87
90
 
88
- def declared_param_is_array?(declared_param)
89
- key = declared_param.to_s
90
- route_options_params[key] && route_options_params[key][:type] == 'Array'
91
- end
91
+ route_options_params = options[:route_options][:params] || {}
92
+ type = route_options_params.dig(key, :type)
93
+ has_children = route_options_params.keys.any? { |k| k != key && k.start_with?(key) }
92
94
 
93
- def route_options_params
94
- options[:route_options][:params] || {}
95
+ if type == 'Hash' && !has_children
96
+ {}
97
+ elsif type == 'Array' || type&.start_with?('[') && !type&.include?(',')
98
+ []
99
+ elsif type == 'Set' || type&.start_with?('#<Set')
100
+ Set.new
101
+ else
102
+ yield
103
+ end
95
104
  end
96
105
 
97
106
  def optioned_param_key(declared_param, options)
@@ -101,10 +110,10 @@ module Grape
101
110
  def optioned_declared_params(**options)
102
111
  declared_params = if options[:include_parent_namespaces]
103
112
  # Declared params including parent namespaces
104
- route_setting(:saved_declared_params).flatten | Array(route_setting(:declared_params))
113
+ route_setting(:declared_params)
105
114
  else
106
115
  # Declared params at current namespace
107
- route_setting(:saved_declared_params).last & Array(route_setting(:declared_params))
116
+ namespace_stackable(:declared_params).last || []
108
117
  end
109
118
 
110
119
  raise ArgumentError, 'Tried to filter for declared parameters but none exist.' unless declared_params
@@ -256,23 +265,36 @@ module Grape
256
265
  body false
257
266
  end
258
267
 
259
- # Allows you to define the response as a file-like object.
268
+ # Deprecated method to send files to the client. Use `sendfile` or `stream`
269
+ def file(value = nil)
270
+ if value.is_a?(String)
271
+ warn '[DEPRECATION] Use sendfile or stream to send files.'
272
+ sendfile(value)
273
+ elsif !value.is_a?(NilClass)
274
+ warn '[DEPRECATION] Use stream to use a Stream object.'
275
+ stream(value)
276
+ else
277
+ warn '[DEPRECATION] Use sendfile or stream to send files.'
278
+ sendfile
279
+ end
280
+ end
281
+
282
+ # Allows you to send a file to the client via sendfile.
260
283
  #
261
284
  # @example
262
285
  # get '/file' do
263
- # file FileStreamer.new(...)
286
+ # sendfile FileStreamer.new(...)
264
287
  # end
265
288
  #
266
289
  # GET /file # => "contents of file"
267
- def file(value = nil)
290
+ def sendfile(value = nil)
268
291
  if value.is_a?(String)
269
- file_body = Grape::ServeFile::FileBody.new(value)
270
- @file = Grape::ServeFile::FileResponse.new(file_body)
292
+ file_body = Grape::ServeStream::FileBody.new(value)
293
+ @stream = Grape::ServeStream::StreamResponse.new(file_body)
271
294
  elsif !value.is_a?(NilClass)
272
- warn '[DEPRECATION] Argument as FileStreamer-like object is deprecated. Use path to file instead.'
273
- @file = Grape::ServeFile::FileResponse.new(value)
295
+ raise ArgumentError, 'Argument must be a file path'
274
296
  else
275
- instance_variable_defined?(:@file) ? @file : nil
297
+ stream
276
298
  end
277
299
  end
278
300
 
@@ -292,10 +314,21 @@ module Grape
292
314
  # * https://github.com/rack/rack/blob/99293fa13d86cd48021630fcc4bd5acc9de5bdc3/lib/rack/chunked.rb
293
315
  # * https://github.com/rack/rack/blob/99293fa13d86cd48021630fcc4bd5acc9de5bdc3/lib/rack/etag.rb
294
316
  def stream(value = nil)
317
+ return if value.nil? && @stream.nil?
318
+
295
319
  header 'Content-Length', nil
296
320
  header 'Transfer-Encoding', nil
297
321
  header 'Cache-Control', 'no-cache' # Skips ETag generation (reading the response up front)
298
- file(value)
322
+ if value.is_a?(String)
323
+ file_body = Grape::ServeStream::FileBody.new(value)
324
+ @stream = Grape::ServeStream::StreamResponse.new(file_body)
325
+ elsif value.respond_to?(:each)
326
+ @stream = Grape::ServeStream::StreamResponse.new(value)
327
+ elsif !value.is_a?(NilClass)
328
+ raise ArgumentError, 'Stream object must respond to :each.'
329
+ else
330
+ @stream
331
+ end
299
332
  end
300
333
 
301
334
  # Allows you to make use of Grape Entities by setting
@@ -388,7 +421,7 @@ module Grape
388
421
  def entity_representation_for(entity_class, object, options)
389
422
  embeds = { env: env }
390
423
  embeds[:version] = env[Grape::Env::API_VERSION] if env[Grape::Env::API_VERSION]
391
- entity_class.represent(object, embeds.merge(options))
424
+ entity_class.represent(object, **embeds.merge(options))
392
425
  end
393
426
  end
394
427
  end
@@ -227,13 +227,17 @@ module Grape
227
227
 
228
228
  alias group requires
229
229
 
230
- def map_params(params, element)
230
+ class EmptyOptionalValue; end
231
+
232
+ def map_params(params, element, is_array = false)
231
233
  if params.is_a?(Array)
232
234
  params.map do |el|
233
- map_params(el, element)
235
+ map_params(el, element, true)
234
236
  end
235
237
  elsif params.is_a?(Hash)
236
- params[element] || {}
238
+ params[element] || (@optional && is_array ? EmptyOptionalValue : {})
239
+ elsif params == EmptyOptionalValue
240
+ EmptyOptionalValue
237
241
  else
238
242
  {}
239
243
  end
@@ -79,7 +79,7 @@ module Grape
79
79
  namespace_inheritable(:do_not_route_options, true)
80
80
  end
81
81
 
82
- def mount(mounts, opts = {})
82
+ def mount(mounts, **opts)
83
83
  mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair)
84
84
  mounts.each_pair do |app, path|
85
85
  if app.respond_to?(:mount_instance)
@@ -170,9 +170,7 @@ module Grape
170
170
  previous_namespace_description = @namespace_description
171
171
  @namespace_description = (@namespace_description || {}).deep_merge(namespace_setting(:description) || {})
172
172
  nest(block) do
173
- if space
174
- namespace_stackable(:namespace, Namespace.new(space, **options))
175
- end
173
+ namespace_stackable(:namespace, Namespace.new(space, **options)) if space
176
174
  end
177
175
  @namespace_description = previous_namespace_description
178
176
  end
@@ -10,7 +10,24 @@ module Grape
10
10
  include Grape::DSL::Configuration
11
11
 
12
12
  module ClassMethods
13
- # Clears all defined parameters and validations.
13
+ # Clears all defined parameters and validations. The main purpose of it is to clean up
14
+ # settings, so next endpoint won't interfere with previous one.
15
+ #
16
+ # params do
17
+ # # params for the endpoint below this block
18
+ # end
19
+ # post '/current' do
20
+ # # whatever
21
+ # end
22
+ #
23
+ # # somewhere between them the reset_validations! method gets called
24
+ #
25
+ # params do
26
+ # # params for the endpoint below this block
27
+ # end
28
+ # post '/next' do
29
+ # # whatever
30
+ # end
14
31
  def reset_validations!
15
32
  unset_namespace_stackable :declared_params
16
33
  unset_namespace_stackable :validations
@@ -16,5 +16,5 @@ Grape::Parser.eager_load!
16
16
  Grape::DSL.eager_load!
17
17
  Grape::API.eager_load!
18
18
  Grape::Presenters.eager_load!
19
- Grape::ServeFile.eager_load!
19
+ Grape::ServeStream.eager_load!
20
20
  Rack::Head # AutoLoads the Rack::Head
@@ -80,7 +80,10 @@ module Grape
80
80
 
81
81
  self.inheritable_setting = new_settings.point_in_time_copy
82
82
 
83
- route_setting(:saved_declared_params, namespace_stackable(:declared_params))
83
+ # now +namespace_stackable(:declared_params)+ contains all params defined for
84
+ # this endpoint and its parents, but later it will be cleaned up,
85
+ # see +reset_validations!+ in lib/grape/dsl/validations.rb
86
+ route_setting(:declared_params, namespace_stackable(:declared_params).flatten)
84
87
  route_setting(:saved_validations, namespace_stackable(:validations))
85
88
 
86
89
  namespace_stackable(:representations, []) unless namespace_stackable(:representations)
@@ -99,7 +102,7 @@ module Grape
99
102
  @block = nil
100
103
 
101
104
  @status = nil
102
- @file = nil
105
+ @stream = nil
103
106
  @body = nil
104
107
  @proc = nil
105
108
 
@@ -116,7 +119,6 @@ module Grape
116
119
  parent_declared_params = namespace_stackable[:declared_params]
117
120
 
118
121
  if parent_declared_params
119
- inheritable_setting.route[:declared_params] ||= []
120
122
  inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten)
121
123
  end
122
124
 
@@ -190,7 +192,7 @@ module Grape
190
192
  requirements: prepare_routes_requirements,
191
193
  prefix: namespace_inheritable(:root_prefix),
192
194
  anchor: options[:route_options].fetch(:anchor, true),
193
- settings: inheritable_setting.route.except(:saved_declared_params, :saved_validations),
195
+ settings: inheritable_setting.route.except(:declared_params, :saved_validations),
194
196
  forward_match: options[:forward_match]
195
197
  }
196
198
  end
@@ -271,8 +273,8 @@ module Grape
271
273
  # status verifies body presence when DELETE
272
274
  @body ||= response_object
273
275
 
274
- # The body commonly is an Array of Strings, the application instance itself, or a File-like object
275
- response_object = file || [body]
276
+ # The body commonly is an Array of Strings, the application instance itself, or a Stream-like object
277
+ response_object = stream || [body]
276
278
 
277
279
  [status, header, response_object]
278
280
  ensure
@@ -21,6 +21,7 @@ module Grape
21
21
  OPTIONS = 'OPTIONS'
22
22
 
23
23
  SUPPORTED_METHODS = [GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS].freeze
24
+ SUPPORTED_METHODS_WITHOUT_OPTIONS = Grape::Util::LazyObject.new { [GET, POST, PUT, PATCH, DELETE, HEAD].freeze }
24
25
 
25
26
  HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION'
26
27
  X_CASCADE = 'X-Cascade'
@@ -8,6 +8,7 @@ module Grape
8
8
  include Helpers
9
9
 
10
10
  attr_reader :app, :env, :options
11
+
11
12
  TEXT_HTML = 'text/html'
12
13
 
13
14
  include Grape::DSL::Headers
@@ -16,7 +17,7 @@ module Grape
16
17
  # @param [Hash] options A hash of options, simply stored for use by subclasses.
17
18
  def initialize(app, **options)
18
19
  @app = app
19
- @options = default_options.merge(**options)
20
+ @options = default_options.merge(options)
20
21
  @app_response = nil
21
22
  end
22
23