grape 2.0.0 → 2.1.3

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