grape 3.2.1 → 3.3.0

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -0
  3. data/README.md +116 -43
  4. data/UPGRADING.md +336 -1
  5. data/grape.gemspec +5 -5
  6. data/lib/grape/api/instance.rb +7 -7
  7. data/lib/grape/api.rb +22 -25
  8. data/lib/grape/cookies.rb +2 -6
  9. data/lib/grape/declared_params_handler.rb +48 -50
  10. data/lib/grape/dsl/callbacks.rb +9 -3
  11. data/lib/grape/dsl/desc.rb +8 -2
  12. data/lib/grape/dsl/entity.rb +88 -0
  13. data/lib/grape/dsl/helpers.rb +27 -7
  14. data/lib/grape/dsl/inside_route.rb +38 -129
  15. data/lib/grape/dsl/logger.rb +3 -5
  16. data/lib/grape/dsl/parameters.rb +32 -38
  17. data/lib/grape/dsl/request_response.rb +53 -48
  18. data/lib/grape/dsl/rescue_options.rb +24 -0
  19. data/lib/grape/dsl/routing.rb +51 -35
  20. data/lib/grape/dsl/settings.rb +14 -8
  21. data/lib/grape/dsl/version_options.rb +23 -0
  22. data/lib/grape/endpoint/options.rb +19 -0
  23. data/lib/grape/endpoint.rb +96 -68
  24. data/lib/grape/env.rb +1 -3
  25. data/lib/grape/error_formatter/base.rb +23 -20
  26. data/lib/grape/error_formatter/json.rb +8 -4
  27. data/lib/grape/error_formatter/txt.rb +10 -10
  28. data/lib/grape/exceptions/base.rb +3 -1
  29. data/lib/grape/exceptions/error_response.rb +45 -0
  30. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  31. data/lib/grape/exceptions/validation.rb +14 -0
  32. data/lib/grape/exceptions/validation_array_errors.rb +4 -0
  33. data/lib/grape/exceptions/validation_errors.rb +12 -20
  34. data/lib/grape/formatter/serializable_hash.rb +5 -9
  35. data/lib/grape/json.rb +38 -2
  36. data/lib/grape/locale/en.yml +2 -0
  37. data/lib/grape/middleware/auth/base.rb +2 -3
  38. data/lib/grape/middleware/auth/dsl.rb +23 -8
  39. data/lib/grape/middleware/base.rb +22 -33
  40. data/lib/grape/middleware/deprecated_options_hash_access.rb +19 -0
  41. data/lib/grape/middleware/error.rb +152 -62
  42. data/lib/grape/middleware/formatter.rb +66 -50
  43. data/lib/grape/middleware/precomputed_content_types.rb +46 -0
  44. data/lib/grape/middleware/stack.rb +5 -6
  45. data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
  46. data/lib/grape/middleware/versioner/base.rb +34 -38
  47. data/lib/grape/middleware/versioner/header.rb +3 -5
  48. data/lib/grape/middleware/versioner/path.rb +8 -3
  49. data/lib/grape/namespace.rb +3 -3
  50. data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
  51. data/lib/grape/parser/json.rb +1 -1
  52. data/lib/grape/path.rb +14 -17
  53. data/lib/grape/request.rb +15 -8
  54. data/lib/grape/router/mustermann_pattern.rb +44 -0
  55. data/lib/grape/router/pattern.rb +6 -10
  56. data/lib/grape/router.rb +28 -42
  57. data/lib/grape/serve_stream/file_body.rb +1 -0
  58. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  59. data/lib/grape/serve_stream/stream_response.rb +1 -0
  60. data/lib/grape/testing.rb +33 -0
  61. data/lib/grape/util/base_inheritable.rb +13 -16
  62. data/lib/grape/util/inheritable_setting.rb +44 -27
  63. data/lib/grape/util/inheritable_values.rb +7 -3
  64. data/lib/grape/util/lazy/base.rb +16 -0
  65. data/lib/grape/util/lazy/block.rb +2 -9
  66. data/lib/grape/util/lazy/value.rb +2 -9
  67. data/lib/grape/util/lazy/value_enumerable.rb +13 -16
  68. data/lib/grape/util/media_type.rb +1 -4
  69. data/lib/grape/util/path_normalizer.rb +34 -0
  70. data/lib/grape/util/registry.rb +1 -1
  71. data/lib/grape/util/stackable_values.rb +11 -8
  72. data/lib/grape/validations/attributes_iterator.rb +13 -13
  73. data/lib/grape/validations/coerce_options.rb +21 -0
  74. data/lib/grape/validations/oneof_collector.rb +39 -0
  75. data/lib/grape/validations/param_scope_tracker.rb +14 -9
  76. data/lib/grape/validations/params_documentation.rb +25 -23
  77. data/lib/grape/validations/params_scope.rb +54 -172
  78. data/lib/grape/validations/shared_options.rb +19 -0
  79. data/lib/grape/validations/types/array_coercer.rb +2 -2
  80. data/lib/grape/validations/types/custom_type_coercer.rb +41 -85
  81. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  82. data/lib/grape/validations/types/dry_type_coercer.rb +3 -3
  83. data/lib/grape/validations/types/primitive_coercer.rb +10 -5
  84. data/lib/grape/validations/types/set_coercer.rb +1 -1
  85. data/lib/grape/validations/types/variant_collection_coercer.rb +8 -0
  86. data/lib/grape/validations/types.rb +23 -30
  87. data/lib/grape/validations/validations_spec.rb +149 -0
  88. data/lib/grape/validations/validators/all_or_none_of_validator.rb +1 -1
  89. data/lib/grape/validations/validators/at_least_one_of_validator.rb +1 -1
  90. data/lib/grape/validations/validators/base.rb +39 -22
  91. data/lib/grape/validations/validators/coerce_validator.rb +5 -3
  92. data/lib/grape/validations/validators/default_validator.rb +7 -8
  93. data/lib/grape/validations/validators/except_values_validator.rb +3 -2
  94. data/lib/grape/validations/validators/length_validator.rb +1 -1
  95. data/lib/grape/validations/validators/multiple_params_base.rb +10 -7
  96. data/lib/grape/validations/validators/oneof_validator.rb +49 -0
  97. data/lib/grape/validations/validators/values_validator.rb +5 -5
  98. data/lib/grape/version.rb +1 -1
  99. data/lib/grape/xml.rb +8 -1
  100. data/lib/grape.rb +6 -6
  101. metadata +34 -18
  102. data/lib/grape/middleware/globals.rb +0 -14
@@ -3,18 +3,50 @@
3
3
  module Grape
4
4
  module Middleware
5
5
  class Error < Base
6
- DEFAULT_OPTIONS = {
7
- default_status: 500,
8
- default_message: '',
9
- format: :txt,
10
- rescue_all: false,
11
- rescue_grape_exceptions: false,
12
- rescue_subclasses: true,
13
- rescue_options: {
14
- backtrace: false,
15
- original_exception: false
16
- }.freeze
17
- }.freeze
6
+ extend Forwardable
7
+ include PrecomputedContentTypes
8
+
9
+ Options = Data.define(
10
+ :all_rescue_handler, :base_only_rescue_handlers, :content_types,
11
+ :default_error_formatter, :default_message, :default_status,
12
+ :error_formatters, :format,
13
+ :grape_exceptions_rescue_handler, :internal_grape_exceptions_rescue_handler,
14
+ :rescue_all, :rescue_grape_exceptions, :rescue_handlers, :rescue_options
15
+ ) do
16
+ include Grape::Middleware::DeprecatedOptionsHashAccess
17
+
18
+ def initialize(
19
+ all_rescue_handler: nil, base_only_rescue_handlers: nil, content_types: nil,
20
+ default_error_formatter: nil, default_message: '', default_status: 500,
21
+ error_formatters: nil, format: :txt,
22
+ grape_exceptions_rescue_handler: nil, internal_grape_exceptions_rescue_handler: nil,
23
+ rescue_all: false, rescue_grape_exceptions: false, rescue_handlers: nil,
24
+ rescue_options: nil
25
+ )
26
+ # `rescue_options:` arrives nil from `Endpoint#error_middleware_options`
27
+ # when no `rescue_from` has been called — fall back to the documented
28
+ # defaults rather than letting nil propagate to `def_delegator
29
+ # :rescue_options, :backtrace`.
30
+ rescue_options ||= Grape::DSL::RescueOptions.new
31
+ super
32
+ end
33
+ end
34
+
35
+ # @deprecated Kept as a frozen Hash representation of the {Options}
36
+ # defaults for back-compat. Will be removed in a future release.
37
+ DEFAULT_OPTIONS = Options.new.to_h.freeze
38
+
39
+ def_delegators :config,
40
+ :all_rescue_handler, :base_only_rescue_handlers, :default_error_formatter,
41
+ :default_message, :default_status, :error_formatters, :format,
42
+ :grape_exceptions_rescue_handler, :internal_grape_exceptions_rescue_handler,
43
+ :rescue_all, :rescue_grape_exceptions, :rescue_handlers, :rescue_options
44
+
45
+ # +:backtrace+ / +:original_exception+ on the rescue options become
46
+ # +#include_backtrace+ / +#include_original_exception+ on the middleware,
47
+ # which is what the formatter call site reads.
48
+ def_delegator :rescue_options, :backtrace, :include_backtrace
49
+ def_delegator :rescue_options, :original_exception, :include_original_exception
18
50
 
19
51
  def call!(env)
20
52
  @env = env
@@ -30,52 +62,57 @@ module Grape
30
62
  Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), Grape::Util::Header.new.merge(headers))
31
63
  end
32
64
 
33
- def format_message(message, backtrace, original_exception = nil)
34
- format = env[Grape::Env::API_FORMAT] || options[:format]
35
- formatter = Grape::ErrorFormatter.formatter_for(format, options[:error_formatters], options[:default_error_formatter])
36
- return formatter.call(message, backtrace, options, env, original_exception) if formatter
65
+ def format_message(error)
66
+ current_format = env[Grape::Env::API_FORMAT] || format
67
+ formatter = Grape::ErrorFormatter.formatter_for(current_format, error_formatters, default_error_formatter)
68
+ return formatter.call(error:, env:, include_backtrace:, include_original_exception:) if formatter
37
69
 
38
- throw :error,
39
- status: 406,
40
- message: "The requested format '#{format}' is not supported.",
41
- backtrace:,
42
- original_exception:
70
+ throw :error, Grape::Exceptions::ErrorResponse.new(
71
+ status: 406,
72
+ message: "The requested format '#{current_format}' is not supported.",
73
+ backtrace: error.backtrace,
74
+ original_exception: error.original_exception
75
+ )
43
76
  end
44
77
 
45
78
  def find_handler(klass)
46
- rescue_handler_for_base_only_class(klass) ||
47
- rescue_handler_for_class_or_its_ancestor(klass) ||
79
+ registered_rescue_handler(klass) ||
48
80
  rescue_handler_for_grape_exception(klass) ||
49
81
  rescue_handler_for_any_class(klass) ||
50
82
  raise
51
83
  end
52
84
 
53
- def error_response(error = {})
54
- status = error[:status] || options[:default_status]
55
- env[Grape::Env::API_ENDPOINT].status(status) # error! may not have been called
56
- message = error[:message] || options[:default_message]
57
- headers = { Rack::CONTENT_TYPE => content_type }.tap do |h|
58
- h.merge!(error[:headers]) if error[:headers].is_a?(Hash)
59
- end
60
- backtrace = error[:backtrace] || error[:original_exception]&.backtrace || []
61
- original_exception = error.is_a?(Exception) ? error : error[:original_exception]
62
- rack_response(status, headers, format_message(message, backtrace, original_exception))
85
+ def error_response(error = nil)
86
+ raw = Grape::Exceptions::ErrorResponse.coerce(error)
87
+ headers = { Rack::CONTENT_TYPE => content_type }
88
+ headers.merge!(raw.headers) if raw.headers.is_a?(Hash)
89
+ payload = raw.with(
90
+ status: raw.status || default_status,
91
+ message: raw.message || default_message,
92
+ headers:,
93
+ backtrace: raw.backtrace || raw.original_exception&.backtrace || []
94
+ )
95
+ env[Grape::Env::API_ENDPOINT].status(payload.status) # error! may not have been called
96
+ rack_response(payload.status, payload.headers, format_message(payload))
63
97
  end
64
98
 
65
99
  def default_rescue_handler(exception)
66
- error_response(message: exception.message, backtrace: exception.backtrace, original_exception: exception)
100
+ error_response(
101
+ Grape::Exceptions::ErrorResponse.new(
102
+ message: exception.message,
103
+ backtrace: exception.backtrace,
104
+ original_exception: exception
105
+ )
106
+ )
67
107
  end
68
108
 
69
- def rescue_handler_for_base_only_class(klass)
70
- error, handler = options[:base_only_rescue_handlers]&.find { |err, _handler| klass == err }
71
-
72
- return unless error
73
-
74
- handler || method(:default_rescue_handler)
109
+ def registered_rescue_handler(klass)
110
+ rescue_handler_from(base_only_rescue_handlers) { |err| klass == err } ||
111
+ rescue_handler_from(rescue_handlers) { |err| klass <= err }
75
112
  end
76
113
 
77
- def rescue_handler_for_class_or_its_ancestor(klass)
78
- error, handler = options[:rescue_handlers]&.find { |err, _handler| klass <= err }
114
+ def rescue_handler_from(handlers)
115
+ error, handler = handlers&.find { |err, _handler| yield(err) }
79
116
 
80
117
  return unless error
81
118
 
@@ -85,45 +122,98 @@ module Grape
85
122
  def rescue_handler_for_grape_exception(klass)
86
123
  return unless klass <= Grape::Exceptions::Base
87
124
  return method(:error_response) if klass == Grape::Exceptions::InvalidVersionHeader
88
- return unless options[:rescue_grape_exceptions] || !options[:rescue_all]
125
+ return unless rescue_grape_exceptions || !rescue_all
89
126
 
90
- options[:grape_exceptions_rescue_handler] || method(:error_response)
127
+ grape_exceptions_rescue_handler || method(:error_response)
91
128
  end
92
129
 
93
130
  def rescue_handler_for_any_class(klass)
94
131
  return unless klass <= StandardError
95
- return unless options[:rescue_all] || options[:rescue_grape_exceptions]
132
+ return unless rescue_all || rescue_grape_exceptions
96
133
 
97
- options[:all_rescue_handler] || method(:default_rescue_handler)
134
+ all_rescue_handler || method(:default_rescue_handler)
98
135
  end
99
136
 
100
- def run_rescue_handler(handler, error, endpoint)
101
- handler = endpoint.public_method(handler) if handler.instance_of?(Symbol)
137
+ def run_rescue_handler(handler, error, endpoint, redispatched: false)
138
+ handler = endpoint.public_method(handler) if handler.is_a?(Symbol)
102
139
  response = catch(:error) do
103
140
  handler.arity.zero? ? endpoint.instance_exec(&handler) : endpoint.instance_exec(error, &handler)
141
+ rescue StandardError => e
142
+ return redispatch(e, endpoint, redispatched)
104
143
  end
105
144
 
106
- if error?(response)
107
- error_response(response)
108
- elsif response.is_a?(Rack::Response)
109
- response
110
- else
111
- run_rescue_handler(method(:default_rescue_handler), Grape::Exceptions::InvalidResponse.new, endpoint)
112
- end
145
+ return error_response(response) if error?(response)
146
+ return response if response.is_a?(Rack::Response)
147
+
148
+ run_rescue_handler(method(:default_rescue_handler), Grape::Exceptions::InvalidResponse.new, endpoint)
113
149
  end
114
150
 
115
- def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil)
151
+ # Route an exception raised inside a +rescue_from+ block.
152
+ #
153
+ # * If we have already redispatched once (the redispatched handler
154
+ # itself raised), go straight to {#framework_default} — bounds the
155
+ # chain at one redispatch.
156
+ # * Else if the exception has a registered +rescue_from+ handler,
157
+ # run it.
158
+ # * Else if it's a +Grape::Exceptions::Base+ subclass, render it
159
+ # through +error_response+ with its own +status+ and +message+.
160
+ # * Else fall through to {#safe_default}, which lets the user opt
161
+ # in via +rescue_from :internal_grape_exceptions+ or, failing
162
+ # that, applies the framework default.
163
+ def redispatch(error, endpoint, already_redispatched)
164
+ return framework_default(endpoint) if already_redispatched
165
+
166
+ registered = registered_rescue_handler(error.class)
167
+
168
+ return run_rescue_handler(registered, error, endpoint, redispatched: true) if registered
169
+ return run_rescue_handler(method(:error_response), error, endpoint, redispatched: true) if error.is_a?(Grape::Exceptions::Base)
170
+
171
+ safe_default(error, endpoint)
172
+ end
173
+
174
+ # The unrecognised-error path. Exposes the original exception on
175
+ # the rack env so upstream Rack middleware (loggers, error
176
+ # trackers) can observe it. If the user registered a
177
+ # +rescue_from :internal_grape_exceptions+ handler, that handler
178
+ # runs and owns the response. Otherwise the framework renders the
179
+ # generic +InternalServerError+ — never the original exception's
180
+ # message. The framework deliberately does no logging of its own
181
+ # here; that's the application's call.
182
+ def safe_default(error, endpoint)
183
+ env[Grape::Env::GRAPE_EXCEPTION] = error
184
+ return run_rescue_handler(internal_grape_exceptions_rescue_handler, error, endpoint, redispatched: true) if internal_grape_exceptions_rescue_handler
185
+
186
+ framework_default(endpoint)
187
+ end
188
+
189
+ def framework_default(endpoint)
190
+ run_rescue_handler(method(:default_rescue_handler), Grape::Exceptions::InternalServerError.new, endpoint)
191
+ end
192
+
193
+ def error!(message, status = default_status, headers = {}, backtrace = [], original_exception = nil)
116
194
  env[Grape::Env::API_ENDPOINT].status(status) # not error! inside route
117
- rack_response(
118
- status, headers.reverse_merge(Rack::CONTENT_TYPE => content_type),
119
- format_message(message, backtrace, original_exception)
195
+ merged_headers = { Rack::CONTENT_TYPE => content_type }.merge!(headers)
196
+ error = Grape::Exceptions::ErrorResponse.new(
197
+ status:, message:, headers: merged_headers, backtrace:, original_exception:
120
198
  )
199
+ rack_response(status, merged_headers, format_message(error))
121
200
  end
122
201
 
123
202
  def error?(response)
124
- return false unless response.is_a?(Hash)
125
-
126
- response.key?(:message) && response.key?(:status) && response.key?(:headers)
203
+ case response
204
+ when Grape::Exceptions::ErrorResponse
205
+ true
206
+ when Hash
207
+ return false unless response.key?(:message) && response.key?(:status) && response.key?(:headers)
208
+
209
+ Grape.deprecator.warn(
210
+ 'Returning or throwing a Hash from a rescue handler is deprecated. ' \
211
+ 'Use `error!(...)` or a `Grape::Exceptions::ErrorResponse` instead.'
212
+ )
213
+ true
214
+ else
215
+ false
216
+ end
127
217
  end
128
218
  end
129
219
  end
@@ -3,12 +3,25 @@
3
3
  module Grape
4
4
  module Middleware
5
5
  class Formatter < Base
6
- DEFAULT_OPTIONS = {
7
- default_format: :txt
8
- }.freeze
6
+ extend Forwardable
7
+ include PrecomputedContentTypes
8
+
9
+ Options = Data.define(:content_types, :default_format, :format, :formatters, :parsers) do
10
+ include Grape::Middleware::DeprecatedOptionsHashAccess
11
+
12
+ def initialize(content_types: nil, default_format: :txt, format: nil, formatters: nil, parsers: nil)
13
+ super
14
+ end
15
+ end
16
+
17
+ # @deprecated Kept as a frozen Hash representation of the {Options}
18
+ # defaults for back-compat. Will be removed in a future release.
19
+ DEFAULT_OPTIONS = Options.new.to_h.freeze
9
20
 
10
21
  ALL_MEDIA_TYPES = '*/*'
11
22
 
23
+ def_delegators :config, :default_format, :format, :formatters, :parsers
24
+
12
25
  def before
13
26
  negotiate_content_type
14
27
  read_body_input
@@ -17,13 +30,11 @@ module Grape
17
30
  def after
18
31
  return unless @app_response
19
32
 
20
- status, headers, bodies = *@app_response
33
+ status, headers, bodies = @app_response
21
34
 
22
- if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
23
- [status, headers, []]
24
- else
25
- build_formatted_response(status, headers, bodies)
26
- end
35
+ return [status, headers, []] if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
36
+
37
+ build_formatted_response(status, headers, bodies)
27
38
  end
28
39
 
29
40
  private
@@ -37,19 +48,28 @@ module Grape
37
48
  end
38
49
  else
39
50
  # Allow content-type to be explicitly overwritten
40
- formatter = fetch_formatter(headers, options)
41
- bodymap = ActiveSupport::Notifications.instrument('format_response.grape', formatter:, env:) do
42
- bodies.collect { |body| formatter.call(body, env) }
51
+ formatter = fetch_formatter(headers)
52
+ bodymap = instrument_format_response(formatter) do
53
+ bodies.map { |body| formatter.call(body, env) }
43
54
  end
44
55
  Rack::Response.new(bodymap, status, headers)
45
56
  end
46
57
  rescue Grape::Exceptions::InvalidFormatter => e
47
- throw :error, status: 500, message: e.message, backtrace: e.backtrace, original_exception: e
58
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 500, message: e.message, backtrace: e.backtrace, original_exception: e)
59
+ end
60
+
61
+ # Guards on +listening?+ so that with no subscriber the payload Hash and
62
+ # notification machinery are skipped and the block runs directly (no added
63
+ # allocations); the block is forwarded anonymously.
64
+ def instrument_format_response(formatter, &)
65
+ return yield unless ActiveSupport::Notifications.notifier.listening?('format_response.grape')
66
+
67
+ ActiveSupport::Notifications.instrument('format_response.grape', formatter:, env:, &)
48
68
  end
49
69
 
50
- def fetch_formatter(headers, options)
70
+ def fetch_formatter(headers)
51
71
  api_format = env.fetch(Grape::Env::API_FORMAT) { mime_types[headers[Rack::CONTENT_TYPE]] }
52
- Grape::Formatter.formatter_for(api_format, options[:formatters])
72
+ Grape::Formatter.formatter_for(api_format, formatters)
53
73
  end
54
74
 
55
75
  # Set the content type header for the API format if it is not already present.
@@ -57,11 +77,10 @@ module Grape
57
77
  # @param headers [Hash]
58
78
  # @return [Hash]
59
79
  def ensure_content_type(headers)
60
- if headers[Rack::CONTENT_TYPE]
61
- headers
62
- else
63
- headers.merge(Rack::CONTENT_TYPE => content_type_for(env[Grape::Env::API_FORMAT]))
64
- end
80
+ return headers if headers[Rack::CONTENT_TYPE]
81
+
82
+ headers[Rack::CONTENT_TYPE] = content_type_for(env[Grape::Env::API_FORMAT])
83
+ headers
65
84
  end
66
85
 
67
86
  def read_body_input
@@ -84,28 +103,26 @@ module Grape
84
103
  return if body.empty?
85
104
 
86
105
  media_type = rack_request.media_type
87
- fmt = media_type ? mime_types[media_type] : options[:default_format]
88
-
89
- throw :error, status: 415, message: "The provided content-type '#{media_type}' is not supported." unless content_type_for(fmt)
90
- parser = Grape::Parser.parser_for fmt, options[:parsers]
91
- if parser
92
- begin
93
- body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
94
- if body.is_a?(Hash)
95
- env[Rack::RACK_REQUEST_FORM_HASH] = if env.key?(Rack::RACK_REQUEST_FORM_HASH)
96
- env[Rack::RACK_REQUEST_FORM_HASH].merge(body)
97
- else
98
- body
99
- end
100
- env[Rack::RACK_REQUEST_FORM_INPUT] = env[Rack::RACK_INPUT]
106
+ fmt = media_type ? mime_types[media_type] : default_format
107
+
108
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 415, message: "The provided content-type '#{media_type}' is not supported.") unless content_type_for(fmt)
109
+ parser = Grape::Parser.parser_for fmt, parsers
110
+ return env[Grape::Env::API_REQUEST_BODY] = body unless parser
111
+
112
+ begin
113
+ body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
114
+ if body.is_a?(Hash)
115
+ if (form_hash = env[Rack::RACK_REQUEST_FORM_HASH])
116
+ form_hash.merge!(body)
117
+ else
118
+ env[Rack::RACK_REQUEST_FORM_HASH] = body
101
119
  end
102
- rescue Grape::Exceptions::Base => e
103
- raise e
104
- rescue StandardError => e
105
- throw :error, status: 400, message: e.message, backtrace: e.backtrace, original_exception: e
120
+ env[Rack::RACK_REQUEST_FORM_INPUT] = env[Rack::RACK_INPUT]
106
121
  end
107
- else
108
- env[Grape::Env::API_REQUEST_BODY] = body
122
+ rescue Grape::Exceptions::Base => e
123
+ raise e
124
+ rescue StandardError => e
125
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 400, message: e.message, backtrace: e.backtrace, original_exception: e)
109
126
  end
110
127
  end
111
128
 
@@ -116,19 +133,18 @@ module Grape
116
133
  # - multipart/related
117
134
  # - multipart/mixed
118
135
  def read_body_input?
119
- (rack_request.post? || rack_request.put? || rack_request.patch? || rack_request.delete?) &&
120
- !(rack_request.form_data? && rack_request.content_type) &&
121
- !rack_request.parseable_data? &&
122
- (rack_request.content_length.to_i.positive? || rack_request.env['HTTP_TRANSFER_ENCODING'] == 'chunked')
136
+ return false unless rack_request.post? || rack_request.put? || rack_request.patch? || rack_request.delete?
137
+ return false if rack_request.form_data? && rack_request.content_type
138
+ return false if rack_request.parseable_data?
139
+
140
+ rack_request.content_length.to_i.positive? || rack_request.env['HTTP_TRANSFER_ENCODING'] == 'chunked'
123
141
  end
124
142
 
125
143
  def negotiate_content_type
126
- fmt = format_from_extension || query_params['format'] || options[:format] || format_from_header || options[:default_format]
127
- if content_type_for(fmt)
128
- env[Grape::Env::API_FORMAT] = fmt.to_sym
129
- else
130
- throw :error, status: 406, message: "The requested format '#{fmt}' is not supported."
131
- end
144
+ fmt = format_from_extension || query_params['format'] || format || format_from_header || default_format
145
+ return env[Grape::Env::API_FORMAT] = fmt.to_sym if content_type_for(fmt)
146
+
147
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 406, message: "The requested format '#{fmt}' is not supported.")
132
148
  end
133
149
 
134
150
  def format_from_extension
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Middleware
5
+ # Include in a middleware subclass that needs content-type negotiation.
6
+ # Provides +content_types+ / +mime_types+ / +content_type_for+ /
7
+ # +content_type+ resolved from +config.content_types+ and
8
+ # +config.format+ — so the consuming middleware's +Options+ Data class
9
+ # must declare both fields. Warms those caches on the parent instance
10
+ # at initialization so per-request +dup+s inherit them (avoiding
11
+ # ~1 µs/request of +with_indifferent_access+ recomputation).
12
+ #
13
+ # Opt-in: plain +Grape::Middleware::Base+ subclasses that don't need
14
+ # content-type-aware helpers don't pay for them.
15
+ module PrecomputedContentTypes
16
+ def initialize(app, **options)
17
+ super
18
+ content_types
19
+ mime_types
20
+ content_types_indifferent_access
21
+ end
22
+
23
+ def content_types
24
+ @content_types ||= Grape::ContentTypes.content_types_for(config.content_types)
25
+ end
26
+
27
+ def mime_types
28
+ @mime_types ||= Grape::ContentTypes.mime_types_for(content_types)
29
+ end
30
+
31
+ def content_type_for(format)
32
+ content_types_indifferent_access[format]
33
+ end
34
+
35
+ def content_type
36
+ content_type_for(env[Grape::Env::API_FORMAT] || config.format) || 'text/html'
37
+ end
38
+
39
+ private
40
+
41
+ def content_types_indifferent_access
42
+ @content_types_indifferent_access ||= ActiveSupport::HashWithIndifferentAccess.new(content_types)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -28,6 +28,7 @@ module Grape
28
28
  klass == other || (name.nil? && klass.superclass == other)
29
29
  end
30
30
  end
31
+ alias eql? ==
31
32
 
32
33
  def inspect
33
34
  klass.to_s
@@ -83,12 +84,10 @@ module Grape
83
84
 
84
85
  # @return [Rack::Builder] the builder object with our middlewares applied
85
86
  def build
86
- Rack::Builder.new.tap do |builder|
87
- others.shift(others.size).each { |m| merge_with(m) }
88
- middlewares.each do |m|
89
- m.build(builder)
90
- end
91
- end
87
+ builder = Rack::Builder.new
88
+ others.shift(others.size).each { |m| merge_with(m) }
89
+ middlewares.each { |m| m.build(builder) }
90
+ builder
92
91
  end
93
92
 
94
93
  # @description Add middlewares with :use operation to the stack. Store others with :insert_* operation for later
@@ -30,7 +30,7 @@ module Grape
30
30
  private
31
31
 
32
32
  def not_acceptable!(message)
33
- throw :error, status: 406, headers: error_headers, message:
33
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 406, headers: error_headers, message:)
34
34
  end
35
35
  end
36
36
  end
@@ -4,43 +4,43 @@ module Grape
4
4
  module Middleware
5
5
  module Versioner
6
6
  class Base < Grape::Middleware::Base
7
- DEFAULT_OPTIONS = {
8
- pattern: /.*/i,
9
- prefix: nil,
10
- mount_path: nil,
11
- version_options: {
12
- strict: false,
13
- cascade: true,
14
- parameter: 'apiver',
15
- vendor: nil
16
- }.freeze
17
- }.freeze
7
+ extend Forwardable
8
+ include Grape::Middleware::PrecomputedContentTypes
18
9
 
19
- CASCADE_PASS_HEADER = { 'X-Cascade' => 'pass' }.freeze
10
+ Options = Data.define(
11
+ :content_types, :format, :mount_path, :pattern, :prefix, :version_options, :versions
12
+ ) do
13
+ include Grape::Middleware::DeprecatedOptionsHashAccess
20
14
 
21
- DEFAULT_OPTIONS.each_key do |key|
22
- define_method key do
23
- options[key]
15
+ def initialize(
16
+ content_types: nil, format: nil, mount_path: nil, pattern: /.*/i, prefix: nil,
17
+ version_options: Grape::DSL::VersionOptions.new, versions: nil
18
+ )
19
+ super
24
20
  end
25
21
  end
26
22
 
27
- DEFAULT_OPTIONS[:version_options].each_key do |key|
28
- define_method key do
29
- options[:version_options][key]
30
- end
31
- end
23
+ # @deprecated Kept as a frozen Hash representation of the {Options}
24
+ # defaults for back-compat. Will be removed in a future release.
25
+ DEFAULT_OPTIONS = Options.new.to_h.freeze
26
+
27
+ CASCADE_PASS_HEADER = { 'X-Cascade' => 'pass' }.freeze
32
28
 
33
29
  def self.inherited(klass)
34
30
  super
35
31
  Versioner.register(klass)
36
32
  end
37
33
 
38
- attr_reader :error_headers, :versions
34
+ attr_reader :available_media_types, :error_headers, :versions
35
+
36
+ def_delegators :config, :mount_path, :pattern, :prefix, :version_options
37
+ def_delegators :version_options, :cascade, :parameter, :strict, :vendor
39
38
 
40
39
  def initialize(app, **options)
41
40
  super
41
+ @versions = config.versions&.map(&:to_s) # making sure versions are strings to ease potential match
42
42
  @error_headers = cascade ? CASCADE_PASS_HEADER : {}
43
- @versions = options[:versions]&.map(&:to_s) # making sure versions are strings to ease potential match
43
+ @available_media_types = build_available_media_types
44
44
  end
45
45
 
46
46
  def potential_version_match?(potential_version)
@@ -48,27 +48,23 @@ module Grape
48
48
  end
49
49
 
50
50
  def version_not_found!
51
- throw :error, status: 404, message: '404 API Version Not Found', headers: CASCADE_PASS_HEADER
51
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 404, message: '404 API Version Not Found', headers: CASCADE_PASS_HEADER)
52
52
  end
53
53
 
54
54
  private
55
55
 
56
- def available_media_types
57
- @available_media_types ||= begin
58
- media_types = []
59
- base_media_type = "application/vnd.#{vendor}"
60
- content_types.each_key do |extension|
61
- versions&.reverse_each do |version|
62
- media_types << "#{base_media_type}-#{version}+#{extension}"
63
- media_types << "#{base_media_type}-#{version}"
64
- end
65
- media_types << "#{base_media_type}+#{extension}"
66
- end
67
-
68
- media_types << base_media_type
69
- media_types.concat(content_types.values.flatten)
70
- media_types
56
+ def build_available_media_types
57
+ media_types = []
58
+ base_media_type = "application/vnd.#{vendor}"
59
+ versions&.reverse_each do |version|
60
+ versioned_base = "#{base_media_type}-#{version}"
61
+ content_types.each_key { |extension| media_types << "#{versioned_base}+#{extension}" }
62
+ media_types << versioned_base
71
63
  end
64
+ content_types.each_key { |extension| media_types << "#{base_media_type}+#{extension}" }
65
+ media_types << base_media_type
66
+ media_types.concat(content_types.values.flatten)
67
+ media_types
72
68
  end
73
69
  end
74
70
  end
@@ -41,11 +41,9 @@ module Grape
41
41
 
42
42
  strict_header_checks!
43
43
  media_type = Grape::Util::MediaType.best_quality(accept_header, available_media_types)
44
- if media_type
45
- yield media_type
46
- else
47
- fail!
48
- end
44
+ return yield media_type if media_type
45
+
46
+ fail!
49
47
  end
50
48
 
51
49
  def accept_header