grape 2.0.0 → 2.1.3

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -1
  3. data/README.md +362 -316
  4. data/UPGRADING.md +197 -7
  5. data/grape.gemspec +5 -6
  6. data/lib/grape/api/instance.rb +14 -11
  7. data/lib/grape/api.rb +19 -10
  8. data/lib/grape/content_types.rb +0 -2
  9. data/lib/grape/cookies.rb +2 -1
  10. data/lib/grape/dry_types.rb +0 -2
  11. data/lib/grape/dsl/desc.rb +22 -20
  12. data/lib/grape/dsl/headers.rb +1 -1
  13. data/lib/grape/dsl/inside_route.rb +46 -15
  14. data/lib/grape/dsl/parameters.rb +5 -4
  15. data/lib/grape/dsl/routing.rb +20 -4
  16. data/lib/grape/dsl/validations.rb +13 -0
  17. data/lib/grape/endpoint.rb +14 -17
  18. data/lib/grape/{util/env.rb → env.rb} +0 -5
  19. data/lib/grape/error_formatter/txt.rb +11 -10
  20. data/lib/grape/exceptions/base.rb +3 -3
  21. data/lib/grape/exceptions/validation.rb +0 -2
  22. data/lib/grape/exceptions/validation_array_errors.rb +1 -0
  23. data/lib/grape/exceptions/validation_errors.rb +2 -4
  24. data/lib/grape/extensions/hash.rb +5 -1
  25. data/lib/grape/http/headers.rb +18 -34
  26. data/lib/grape/{util/json.rb → json.rb} +1 -3
  27. data/lib/grape/locale/en.yml +3 -0
  28. data/lib/grape/middleware/auth/base.rb +0 -2
  29. data/lib/grape/middleware/auth/dsl.rb +0 -2
  30. data/lib/grape/middleware/base.rb +1 -3
  31. data/lib/grape/middleware/error.rb +55 -50
  32. data/lib/grape/middleware/formatter.rb +16 -13
  33. data/lib/grape/middleware/globals.rb +1 -3
  34. data/lib/grape/middleware/stack.rb +4 -5
  35. data/lib/grape/middleware/versioner/accept_version_header.rb +0 -2
  36. data/lib/grape/middleware/versioner/header.rb +17 -163
  37. data/lib/grape/middleware/versioner/param.rb +2 -4
  38. data/lib/grape/middleware/versioner/path.rb +1 -3
  39. data/lib/grape/namespace.rb +3 -4
  40. data/lib/grape/path.rb +24 -29
  41. data/lib/grape/request.rb +4 -12
  42. data/lib/grape/router/base_route.rb +39 -0
  43. data/lib/grape/router/greedy_route.rb +20 -0
  44. data/lib/grape/router/pattern.rb +39 -30
  45. data/lib/grape/router/route.rb +22 -59
  46. data/lib/grape/router.rb +32 -37
  47. data/lib/grape/util/accept_header_handler.rb +105 -0
  48. data/lib/grape/util/base_inheritable.rb +4 -4
  49. data/lib/grape/util/cache.rb +0 -3
  50. data/lib/grape/util/endpoint_configuration.rb +1 -1
  51. data/lib/grape/util/header.rb +13 -0
  52. data/lib/grape/util/inheritable_values.rb +0 -2
  53. data/lib/grape/util/lazy/block.rb +29 -0
  54. data/lib/grape/util/lazy/object.rb +45 -0
  55. data/lib/grape/util/lazy/value.rb +38 -0
  56. data/lib/grape/util/lazy/value_array.rb +21 -0
  57. data/lib/grape/util/lazy/value_enumerable.rb +34 -0
  58. data/lib/grape/util/lazy/value_hash.rb +21 -0
  59. data/lib/grape/util/media_type.rb +70 -0
  60. data/lib/grape/util/reverse_stackable_values.rb +1 -6
  61. data/lib/grape/util/stackable_values.rb +1 -6
  62. data/lib/grape/util/strict_hash_configuration.rb +3 -3
  63. data/lib/grape/validations/attributes_doc.rb +38 -36
  64. data/lib/grape/validations/attributes_iterator.rb +1 -0
  65. data/lib/grape/validations/contract_scope.rb +71 -0
  66. data/lib/grape/validations/params_scope.rb +22 -19
  67. data/lib/grape/validations/types/array_coercer.rb +0 -2
  68. data/lib/grape/validations/types/build_coercer.rb +69 -71
  69. data/lib/grape/validations/types/dry_type_coercer.rb +1 -11
  70. data/lib/grape/validations/types/json.rb +0 -2
  71. data/lib/grape/validations/types/primitive_coercer.rb +0 -2
  72. data/lib/grape/validations/types/set_coercer.rb +0 -3
  73. data/lib/grape/validations/types.rb +0 -3
  74. data/lib/grape/validations/validators/base.rb +1 -0
  75. data/lib/grape/validations/validators/default_validator.rb +5 -1
  76. data/lib/grape/validations/validators/exactly_one_of_validator.rb +1 -1
  77. data/lib/grape/validations/validators/length_validator.rb +42 -0
  78. data/lib/grape/validations/validators/values_validator.rb +6 -1
  79. data/lib/grape/validations.rb +3 -7
  80. data/lib/grape/version.rb +1 -1
  81. data/lib/grape/{util/xml.rb → xml.rb} +1 -1
  82. data/lib/grape.rb +30 -274
  83. metadata +30 -37
  84. data/lib/grape/eager_load.rb +0 -20
  85. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
  86. data/lib/grape/router/attribute_translator.rb +0 -63
  87. data/lib/grape/util/lazy_block.rb +0 -27
  88. data/lib/grape/util/lazy_object.rb +0 -43
  89. data/lib/grape/util/lazy_value.rb +0 -91
@@ -30,7 +30,7 @@ module Grape
30
30
  if args.any?
31
31
  options = args.extract_options!
32
32
  options = options.reverse_merge(using: :path)
33
- requested_versions = args.flatten
33
+ requested_versions = args.flatten.map(&:to_s)
34
34
 
35
35
  raise Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.key?(:vendor)
36
36
 
@@ -54,7 +54,7 @@ module Grape
54
54
 
55
55
  # Define a root URL prefix for your entire API.
56
56
  def prefix(prefix = nil)
57
- namespace_inheritable(:root_prefix, prefix)
57
+ namespace_inheritable(:root_prefix, prefix&.to_s)
58
58
  end
59
59
 
60
60
  # Create a scope without affecting the URL.
@@ -85,8 +85,8 @@ module Grape
85
85
  mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair)
86
86
  mounts.each_pair do |app, path|
87
87
  if app.respond_to?(:mount_instance)
88
- opts_with = opts.any? ? opts.shift[:with] : {}
89
- mount({ app.mount_instance(configuration: opts_with) => path })
88
+ opts_with = opts.any? ? opts.first[:with] : {}
89
+ mount({ app.mount_instance(configuration: opts_with) => path }, *opts)
90
90
  next
91
91
  end
92
92
  in_setting = inheritable_setting
@@ -103,6 +103,15 @@ module Grape
103
103
  change!
104
104
  end
105
105
 
106
+ # When trying to mount multiple times the same endpoint, remove the previous ones
107
+ # from the list of endpoints if refresh_already_mounted parameter is true
108
+ refresh_already_mounted = opts.any? ? opts.first[:refresh_already_mounted] : false
109
+ if refresh_already_mounted && !endpoints.empty?
110
+ endpoints.delete_if do |endpoint|
111
+ endpoint.options[:app].to_s == app.to_s
112
+ end
113
+ end
114
+
106
115
  endpoints << Grape::Endpoint.new(
107
116
  in_setting,
108
117
  method: :any,
@@ -225,6 +234,13 @@ module Grape
225
234
  def versions
226
235
  @versions ||= []
227
236
  end
237
+
238
+ private
239
+
240
+ def refresh_mounted_api(mounts, *opts)
241
+ opts << { refresh_already_mounted: true }
242
+ mount(mounts, *opts)
243
+ end
228
244
  end
229
245
  end
230
246
  end
@@ -38,6 +38,19 @@ module Grape
38
38
  def params(&block)
39
39
  Grape::Validations::ParamsScope.new(api: self, type: Hash, &block)
40
40
  end
41
+
42
+ # Declare the contract to be used for the endpoint's parameters.
43
+ # @param contract [Class<Dry::Validation::Contract> | Dry::Schema::Processor]
44
+ # The contract or schema to be used for validation. Optional.
45
+ # @yield a block yielding a new instance of Dry::Schema::Params
46
+ # subclass, allowing to define the schema inline. When the
47
+ # +contract+ parameter is a schema, it will be used as a parent. Optional.
48
+ def contract(contract = nil, &block)
49
+ raise ArgumentError, 'Either contract or block must be provided' unless contract || block
50
+ raise ArgumentError, 'Cannot inherit from contract, only schema' if block && contract.respond_to?(:schema)
51
+
52
+ Grape::Validations::ContractScope.new(self, contract, &block)
53
+ end
41
54
  end
42
55
  end
43
56
  end
@@ -13,8 +13,8 @@ module Grape
13
13
  attr_reader :env, :request, :headers, :params
14
14
 
15
15
  class << self
16
- def new(*args, &block)
17
- self == Endpoint ? Class.new(Endpoint).new(*args, &block) : super
16
+ def new(...)
17
+ self == Endpoint ? Class.new(Endpoint).new(...) : super
18
18
  end
19
19
 
20
20
  def before_each(new_setup = false, &block)
@@ -55,7 +55,7 @@ module Grape
55
55
 
56
56
  proc do |endpoint_instance|
57
57
  ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: endpoint_instance) do
58
- method.bind(endpoint_instance).call
58
+ method.bind_call(endpoint_instance)
59
59
  end
60
60
  end
61
61
  end
@@ -151,7 +151,7 @@ module Grape
151
151
  reset_routes!
152
152
  routes.each do |route|
153
153
  methods = [route.request_method]
154
- methods << Grape::Http::Headers::HEAD if !namespace_inheritable(:do_not_route_head) && route.request_method == Grape::Http::Headers::GET
154
+ methods << Rack::HEAD if !namespace_inheritable(:do_not_route_head) && route.request_method == Rack::GET
155
155
  methods.each do |method|
156
156
  route = Grape::Router::Route.new(method, route.origin, **route.attributes.to_h) unless route.request_method == method
157
157
  router.append(route.apply(self))
@@ -190,10 +190,11 @@ module Grape
190
190
  end
191
191
 
192
192
  def prepare_version
193
- version = namespace_inheritable(:version) || []
193
+ version = namespace_inheritable(:version)
194
+ return unless version
194
195
  return if version.empty?
195
196
 
196
- version.length == 1 ? version.first.to_s : version
197
+ version.length == 1 ? version.first : version
197
198
  end
198
199
 
199
200
  def merge_route_options(**default)
@@ -206,7 +207,7 @@ module Grape
206
207
 
207
208
  def prepare_path(path)
208
209
  path_settings = inheritable_setting.to_hash[:namespace_stackable].merge(inheritable_setting.to_hash[:namespace_inheritable])
209
- Path.prepare(path, namespace, path_settings)
210
+ Path.new(path, namespace, path_settings)
210
211
  end
211
212
 
212
213
  def namespace
@@ -230,15 +231,15 @@ module Grape
230
231
  options[:app].endpoints if options[:app].respond_to?(:endpoints)
231
232
  end
232
233
 
233
- def equals?(e)
234
- (options == e.options) && (inheritable_setting.to_hash == e.inheritable_setting.to_hash)
234
+ def equals?(endpoint)
235
+ (options == endpoint.options) && (inheritable_setting.to_hash == endpoint.inheritable_setting.to_hash)
235
236
  end
236
237
 
237
238
  protected
238
239
 
239
240
  def run
240
241
  ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env) do
241
- @header = {}
242
+ @header = Grape::Util::Header.new
242
243
  @request = Grape::Request.new(env, build_params_with: namespace_inheritable(:build_params_with))
243
244
  @params = @request.params
244
245
  @headers = @request.headers
@@ -401,15 +402,11 @@ module Grape
401
402
 
402
403
  def options?
403
404
  options[:options_route_enabled] &&
404
- env[Grape::Http::Headers::REQUEST_METHOD] == Grape::Http::Headers::OPTIONS
405
+ env[Rack::REQUEST_METHOD] == Rack::OPTIONS
405
406
  end
406
407
 
407
- def method_missing(name, *_args)
408
- raise NoMethodError.new("undefined method `#{name}' for #{self.class} in `#{route.origin}' endpoint")
409
- end
410
-
411
- def respond_to_missing?(method_name, include_private = false)
412
- super
408
+ def inspect
409
+ "#{self.class} in `#{route.origin}' endpoint"
413
410
  end
414
411
  end
415
412
  end
@@ -11,11 +11,6 @@ module Grape
11
11
  API_VENDOR = 'api.vendor'
12
12
  API_FORMAT = 'api.format'
13
13
 
14
- RACK_INPUT = 'rack.input'
15
- RACK_REQUEST_QUERY_HASH = 'rack.request.query_hash'
16
- RACK_REQUEST_FORM_HASH = 'rack.request.form_hash'
17
- RACK_REQUEST_FORM_INPUT = 'rack.request.form_input'
18
-
19
14
  GRAPE_REQUEST = 'grape.request'
20
15
  GRAPE_REQUEST_HEADERS = 'grape.request.headers'
21
16
  GRAPE_REQUEST_PARAMS = 'grape.request.params'
@@ -10,16 +10,17 @@ module Grape
10
10
  message = present(message, env)
11
11
 
12
12
  result = message.is_a?(Hash) ? ::Grape::Json.dump(message) : message
13
- rescue_options = options[:rescue_options] || {}
14
- if rescue_options[:backtrace] && backtrace && !backtrace.empty?
15
- result += "\r\n backtrace:"
16
- result += backtrace.join("\r\n ")
17
- end
18
- if rescue_options[:original_exception] && original_exception
19
- result += "\r\n original exception:"
20
- result += "\r\n #{original_exception.inspect}"
21
- end
22
- result
13
+ Array.wrap(result).tap do |final_result|
14
+ rescue_options = options[:rescue_options] || {}
15
+ if rescue_options[:backtrace] && backtrace.present?
16
+ final_result << 'backtrace:'
17
+ final_result.concat(backtrace)
18
+ end
19
+ if rescue_options[:original_exception] && original_exception
20
+ final_result << 'original exception:'
21
+ final_result << original_exception.inspect
22
+ end
23
+ end.join("\r\n ")
23
24
  end
24
25
  end
25
26
  end
@@ -41,15 +41,15 @@ module Grape
41
41
  end
42
42
 
43
43
  def problem(key, **attributes)
44
- translate_message("#{key}.problem".to_sym, **attributes)
44
+ translate_message(:"#{key}.problem", **attributes)
45
45
  end
46
46
 
47
47
  def summary(key, **attributes)
48
- translate_message("#{key}.summary".to_sym, **attributes)
48
+ translate_message(:"#{key}.summary", **attributes)
49
49
  end
50
50
 
51
51
  def resolution(key, **attributes)
52
- translate_message("#{key}.resolution".to_sym, **attributes)
52
+ translate_message(:"#{key}.resolution", **attributes)
53
53
  end
54
54
 
55
55
  def translate_attributes(keys, **options)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/exceptions/base'
4
-
5
3
  module Grape
6
4
  module Exceptions
7
5
  class Validation < Grape::Exceptions::Base
@@ -6,6 +6,7 @@ module Grape
6
6
  attr_reader :errors
7
7
 
8
8
  def initialize(errors)
9
+ super()
9
10
  @errors = errors
10
11
  end
11
12
  end
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/exceptions/base'
4
-
5
3
  module Grape
6
4
  module Exceptions
7
5
  class ValidationErrors < Grape::Exceptions::Base
8
6
  ERRORS_FORMAT_KEY = 'grape.errors.format'
9
- DEFAULT_ERRORS_FORMAT = '%{attributes} %{message}'
7
+ DEFAULT_ERRORS_FORMAT = '%<attributes>s %<message>s'
10
8
 
11
9
  include Enumerable
12
10
 
@@ -14,7 +12,7 @@ module Grape
14
12
 
15
13
  def initialize(errors: [], headers: {}, **_options)
16
14
  @errors = errors.group_by(&:params)
17
- super message: full_messages.join(', '), status: 400, headers: headers
15
+ super(message: full_messages.join(', '), status: 400, headers: headers)
18
16
  end
19
17
 
20
18
  def each
@@ -12,8 +12,12 @@ module Grape
12
12
 
13
13
  def build_params
14
14
  rack_params.deep_dup.tap do |params|
15
- params.deep_merge!(grape_routing_args) if env.key?(Grape::Env::GRAPE_ROUTING_ARGS)
16
15
  params.deep_symbolize_keys!
16
+
17
+ if env.key?(Grape::Env::GRAPE_ROUTING_ARGS)
18
+ grape_routing_args.deep_symbolize_keys!
19
+ params.deep_merge!(grape_routing_args)
20
+ end
17
21
  end
18
22
  end
19
23
  end
@@ -1,46 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/util/lazy_object'
4
-
5
3
  module Grape
6
4
  module Http
7
5
  module Headers
8
- # https://github.com/rack/rack/blob/master/lib/rack.rb
9
- HTTP_VERSION = 'HTTP_VERSION'
10
- PATH_INFO = 'PATH_INFO'
11
- REQUEST_METHOD = 'REQUEST_METHOD'
12
- QUERY_STRING = 'QUERY_STRING'
13
-
14
- if Grape.lowercase_headers?
15
- ALLOW = 'allow'
16
- LOCATION = 'location'
17
- TRANSFER_ENCODING = 'transfer-encoding'
18
- X_CASCADE = 'x-cascade'
19
- else
20
- ALLOW = 'Allow'
21
- LOCATION = 'Location'
22
- TRANSFER_ENCODING = 'Transfer-Encoding'
23
- X_CASCADE = 'X-Cascade'
24
- end
25
-
26
- GET = 'GET'
27
- POST = 'POST'
28
- PUT = 'PUT'
29
- PATCH = 'PATCH'
30
- DELETE = 'DELETE'
31
- HEAD = 'HEAD'
32
- OPTIONS = 'OPTIONS'
6
+ HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION'
7
+ HTTP_ACCEPT = 'HTTP_ACCEPT'
8
+ HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING'
33
9
 
34
- SUPPORTED_METHODS = [GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS].freeze
35
- SUPPORTED_METHODS_WITHOUT_OPTIONS = Grape::Util::LazyObject.new { [GET, POST, PUT, PATCH, DELETE, HEAD].freeze }
10
+ ALLOW = 'Allow'
11
+ LOCATION = 'Location'
12
+ X_CASCADE = 'X-Cascade'
13
+ TRANSFER_ENCODING = 'Transfer-Encoding'
36
14
 
37
- HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION'
38
- HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING'
39
- HTTP_ACCEPT = 'HTTP_ACCEPT'
15
+ SUPPORTED_METHODS = [
16
+ Rack::GET,
17
+ Rack::POST,
18
+ Rack::PUT,
19
+ Rack::PATCH,
20
+ Rack::DELETE,
21
+ Rack::HEAD,
22
+ Rack::OPTIONS
23
+ ].freeze
40
24
 
41
- FORMAT = 'format'
25
+ SUPPORTED_METHODS_WITHOUT_OPTIONS = (SUPPORTED_METHODS - [Rack::OPTIONS]).freeze
42
26
 
43
- HTTP_HEADERS = Grape::Util::LazyObject.new do
27
+ HTTP_HEADERS = Grape::Util::Lazy::Object.new do
44
28
  common_http_headers = %w[
45
29
  Version
46
30
  Host
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
-
5
3
  module Grape
6
- if Object.const_defined? :MultiJson
4
+ if defined?(::MultiJson)
7
5
  Json = ::MultiJson
8
6
  else
9
7
  Json = ::JSON
@@ -10,6 +10,9 @@ en:
10
10
  values: 'does not have a valid value'
11
11
  except_values: 'has a value not allowed'
12
12
  same_as: 'is not the same as %{parameter}'
13
+ length: 'is expected to have length within %{min} and %{max}'
14
+ length_min: 'is expected to have length greater than or equal to %{min}'
15
+ length_max: 'is expected to have length less than or equal to %{max}'
13
16
  missing_vendor_option:
14
17
  problem: 'missing :vendor option'
15
18
  summary: 'when version using header, you must specify :vendor option'
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rack/auth/basic'
4
-
5
3
  module Grape
6
4
  module Middleware
7
5
  module Auth
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rack/auth/basic'
4
-
5
3
  module Grape
6
4
  module Middleware
7
5
  module Auth
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/dsl/headers'
4
-
5
3
  module Grape
6
4
  module Middleware
7
5
  class Base
@@ -76,7 +74,7 @@ module Grape
76
74
  end
77
75
 
78
76
  def mime_types
79
- @mime_type ||= content_types.each_pair.with_object({}) do |(k, v), types_without_params|
77
+ @mime_types ||= content_types.each_pair.with_object({}) do |(k, v), types_without_params|
80
78
  types_without_params[v.split(';').first] = k
81
79
  end
82
80
  end
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/middleware/base'
4
- require 'active_support/core_ext/string/output_safety'
5
-
6
3
  module Grape
7
4
  module Middleware
8
5
  class Error < Base
@@ -34,66 +31,59 @@ module Grape
34
31
 
35
32
  def call!(env)
36
33
  @env = env
37
- begin
38
- error_response(catch(:error) do
39
- return @app.call(@env)
40
- end)
41
- rescue Exception => e # rubocop:disable Lint/RescueException
42
- handler =
43
- rescue_handler_for_base_only_class(e.class) ||
44
- rescue_handler_for_class_or_its_ancestor(e.class) ||
45
- rescue_handler_for_grape_exception(e.class) ||
46
- rescue_handler_for_any_class(e.class) ||
47
- raise
48
-
49
- run_rescue_handler(handler, e)
50
- end
51
- end
52
-
53
- def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil)
54
- headers = headers.reverse_merge(Rack::CONTENT_TYPE => content_type)
55
- rack_response(format_message(message, backtrace, original_exception), status, headers)
34
+ error_response(catch(:error) { return @app.call(@env) })
35
+ rescue Exception => e # rubocop:disable Lint/RescueException
36
+ run_rescue_handler(find_handler(e.class), e, @env[Grape::Env::API_ENDPOINT])
56
37
  end
57
38
 
58
- def default_rescue_handler(e)
59
- error_response(message: e.message, backtrace: e.backtrace, original_exception: e)
60
- end
61
-
62
- # TODO: This method is deprecated. Refactor out.
63
- def error_response(error = {})
64
- status = error[:status] || options[:default_status]
65
- message = error[:message] || options[:default_message]
66
- headers = { Rack::CONTENT_TYPE => content_type }
67
- headers.merge!(error[:headers]) if error[:headers].is_a?(Hash)
68
- backtrace = error[:backtrace] || error[:original_exception]&.backtrace || []
69
- original_exception = error.is_a?(Exception) ? error : error[:original_exception] || nil
70
- rack_response(format_message(message, backtrace, original_exception), status, headers)
71
- end
39
+ private
72
40
 
73
- def rack_response(message, status = options[:default_status], headers = { Rack::CONTENT_TYPE => content_type })
74
- message = ERB::Util.html_escape(message) if headers[Rack::CONTENT_TYPE] == TEXT_HTML
75
- Rack::Response.new([message], Rack::Utils.status_code(status), headers)
41
+ def rack_response(status, headers, message)
42
+ message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == TEXT_HTML
43
+ Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), Grape::Util::Header.new.merge(headers))
76
44
  end
77
45
 
78
46
  def format_message(message, backtrace, original_exception = nil)
79
47
  format = env[Grape::Env::API_FORMAT] || options[:format]
80
48
  formatter = Grape::ErrorFormatter.formatter_for(format, **options)
49
+ return formatter.call(message, backtrace, options, env, original_exception) if formatter
50
+
81
51
  throw :error,
82
52
  status: 406,
83
53
  message: "The requested format '#{format}' is not supported.",
84
54
  backtrace: backtrace,
85
- original_exception: original_exception unless formatter
86
- formatter.call(message, backtrace, options, env, original_exception)
55
+ original_exception: original_exception
87
56
  end
88
57
 
89
- private
58
+ def find_handler(klass)
59
+ rescue_handler_for_base_only_class(klass) ||
60
+ rescue_handler_for_class_or_its_ancestor(klass) ||
61
+ rescue_handler_for_grape_exception(klass) ||
62
+ rescue_handler_for_any_class(klass) ||
63
+ raise
64
+ end
65
+
66
+ def error_response(error = {})
67
+ status = error[:status] || options[:default_status]
68
+ message = error[:message] || options[:default_message]
69
+ headers = { Rack::CONTENT_TYPE => content_type }.tap do |h|
70
+ h.merge!(error[:headers]) if error[:headers].is_a?(Hash)
71
+ end
72
+ backtrace = error[:backtrace] || error[:original_exception]&.backtrace || []
73
+ original_exception = error.is_a?(Exception) ? error : error[:original_exception] || nil
74
+ rack_response(status, headers, format_message(message, backtrace, original_exception))
75
+ end
76
+
77
+ def default_rescue_handler(exception)
78
+ error_response(message: exception.message, backtrace: exception.backtrace, original_exception: exception)
79
+ end
90
80
 
91
81
  def rescue_handler_for_base_only_class(klass)
92
82
  error, handler = options[:base_only_rescue_handlers].find { |err, _handler| klass == err }
93
83
 
94
84
  return unless error
95
85
 
96
- handler || :default_rescue_handler
86
+ handler || method(:default_rescue_handler)
97
87
  end
98
88
 
99
89
  def rescue_handler_for_class_or_its_ancestor(klass)
@@ -101,39 +91,54 @@ module Grape
101
91
 
102
92
  return unless error
103
93
 
104
- handler || :default_rescue_handler
94
+ handler || method(:default_rescue_handler)
105
95
  end
106
96
 
107
97
  def rescue_handler_for_grape_exception(klass)
108
98
  return unless klass <= Grape::Exceptions::Base
109
- return :error_response if klass == Grape::Exceptions::InvalidVersionHeader
99
+ return method(:error_response) if klass == Grape::Exceptions::InvalidVersionHeader
110
100
  return unless options[:rescue_grape_exceptions] || !options[:rescue_all]
111
101
 
112
- options[:grape_exceptions_rescue_handler] || :error_response
102
+ options[:grape_exceptions_rescue_handler] || method(:error_response)
113
103
  end
114
104
 
115
105
  def rescue_handler_for_any_class(klass)
116
106
  return unless klass <= StandardError
117
107
  return unless options[:rescue_all] || options[:rescue_grape_exceptions]
118
108
 
119
- options[:all_rescue_handler] || :default_rescue_handler
109
+ options[:all_rescue_handler] || method(:default_rescue_handler)
120
110
  end
121
111
 
122
- def run_rescue_handler(handler, error)
112
+ def run_rescue_handler(handler, error, endpoint)
123
113
  if handler.instance_of?(Symbol)
124
114
  raise NoMethodError, "undefined method '#{handler}'" unless respond_to?(handler)
125
115
 
126
116
  handler = public_method(handler)
127
117
  end
128
118
 
129
- response = handler.arity.zero? ? instance_exec(&handler) : instance_exec(error, &handler)
119
+ response = catch(:error) do
120
+ handler.arity.zero? ? endpoint.instance_exec(&handler) : endpoint.instance_exec(error, &handler)
121
+ end
122
+
123
+ response = error!(response[:message], response[:status], response[:headers]) if error?(response)
130
124
 
131
125
  if response.is_a?(Rack::Response)
132
126
  response
133
127
  else
134
- run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new)
128
+ run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new, endpoint)
135
129
  end
136
130
  end
131
+
132
+ def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil)
133
+ rack_response(
134
+ status, headers.reverse_merge(Rack::CONTENT_TYPE => content_type),
135
+ format_message(message, backtrace, original_exception)
136
+ )
137
+ end
138
+
139
+ def error?(response)
140
+ response.is_a?(Hash) && response[:message] && response[:status] && response[:headers]
141
+ end
137
142
  end
138
143
  end
139
144
  end
@@ -1,11 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/middleware/base'
4
-
5
3
  module Grape
6
4
  module Middleware
7
5
  class Formatter < Base
8
6
  CHUNKED = 'chunked'
7
+ FORMAT = 'format'
9
8
 
10
9
  def default_options
11
10
  {
@@ -26,7 +25,7 @@ module Grape
26
25
  status, headers, bodies = *@app_response
27
26
 
28
27
  if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
29
- @app_response
28
+ [status, headers, []]
30
29
  else
31
30
  build_formatted_response(status, headers, bodies)
32
31
  end
@@ -82,14 +81,14 @@ module Grape
82
81
  !request.parseable_data? &&
83
82
  (request.content_length.to_i.positive? || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED)
84
83
 
85
- return unless (input = env[Grape::Env::RACK_INPUT])
84
+ return unless (input = env[Rack::RACK_INPUT])
86
85
 
87
- input.rewind
86
+ rewind_input input
88
87
  body = env[Grape::Env::API_REQUEST_INPUT] = input.read
89
88
  begin
90
89
  read_rack_input(body) if body && !body.empty?
91
90
  ensure
92
- input.rewind
91
+ rewind_input input
93
92
  end
94
93
  end
95
94
 
@@ -103,12 +102,12 @@ module Grape
103
102
  begin
104
103
  body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
105
104
  if body.is_a?(Hash)
106
- env[Grape::Env::RACK_REQUEST_FORM_HASH] = if env.key?(Grape::Env::RACK_REQUEST_FORM_HASH)
107
- env[Grape::Env::RACK_REQUEST_FORM_HASH].merge(body)
108
- else
109
- body
110
- end
111
- env[Grape::Env::RACK_REQUEST_FORM_INPUT] = env[Grape::Env::RACK_INPUT]
105
+ env[Rack::RACK_REQUEST_FORM_HASH] = if env.key?(Rack::RACK_REQUEST_FORM_HASH)
106
+ env[Rack::RACK_REQUEST_FORM_HASH].merge(body)
107
+ else
108
+ body
109
+ end
110
+ env[Rack::RACK_REQUEST_FORM_INPUT] = env[Rack::RACK_INPUT]
112
111
  end
113
112
  rescue Grape::Exceptions::Base => e
114
113
  raise e
@@ -141,7 +140,7 @@ module Grape
141
140
  end
142
141
 
143
142
  def format_from_params
144
- fmt = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[Grape::Http::Headers::FORMAT]
143
+ fmt = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[FORMAT]
145
144
  # avoid symbol memory leak on an unknown format
146
145
  return fmt.to_sym if content_type_for(fmt)
147
146
 
@@ -174,6 +173,10 @@ module Grape
174
173
  .sort_by { |_, quality_preference| -(quality_preference ? quality_preference.to_f : 1.0) }
175
174
  .flat_map { |mime, _| [mime, mime.sub(vendor_prefix_pattern, '')] }
176
175
  end
176
+
177
+ def rewind_input(input)
178
+ input.rewind if input.respond_to?(:rewind)
179
+ end
177
180
  end
178
181
  end
179
182
  end