grape 1.3.1 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
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