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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -0
- data/README.md +116 -43
- data/UPGRADING.md +336 -1
- data/grape.gemspec +5 -5
- data/lib/grape/api/instance.rb +7 -7
- data/lib/grape/api.rb +22 -25
- data/lib/grape/cookies.rb +2 -6
- data/lib/grape/declared_params_handler.rb +48 -50
- data/lib/grape/dsl/callbacks.rb +9 -3
- data/lib/grape/dsl/desc.rb +8 -2
- data/lib/grape/dsl/entity.rb +88 -0
- data/lib/grape/dsl/helpers.rb +27 -7
- data/lib/grape/dsl/inside_route.rb +38 -129
- data/lib/grape/dsl/logger.rb +3 -5
- data/lib/grape/dsl/parameters.rb +32 -38
- data/lib/grape/dsl/request_response.rb +53 -48
- data/lib/grape/dsl/rescue_options.rb +24 -0
- data/lib/grape/dsl/routing.rb +51 -35
- data/lib/grape/dsl/settings.rb +14 -8
- data/lib/grape/dsl/version_options.rb +23 -0
- data/lib/grape/endpoint/options.rb +19 -0
- data/lib/grape/endpoint.rb +96 -68
- data/lib/grape/env.rb +1 -3
- data/lib/grape/error_formatter/base.rb +23 -20
- data/lib/grape/error_formatter/json.rb +8 -4
- data/lib/grape/error_formatter/txt.rb +10 -10
- data/lib/grape/exceptions/base.rb +3 -1
- data/lib/grape/exceptions/error_response.rb +45 -0
- data/lib/grape/exceptions/internal_server_error.rb +16 -0
- data/lib/grape/exceptions/validation.rb +14 -0
- data/lib/grape/exceptions/validation_array_errors.rb +4 -0
- data/lib/grape/exceptions/validation_errors.rb +12 -20
- data/lib/grape/formatter/serializable_hash.rb +5 -9
- data/lib/grape/json.rb +38 -2
- data/lib/grape/locale/en.yml +2 -0
- data/lib/grape/middleware/auth/base.rb +2 -3
- data/lib/grape/middleware/auth/dsl.rb +23 -8
- data/lib/grape/middleware/base.rb +22 -33
- data/lib/grape/middleware/deprecated_options_hash_access.rb +19 -0
- data/lib/grape/middleware/error.rb +152 -62
- data/lib/grape/middleware/formatter.rb +66 -50
- data/lib/grape/middleware/precomputed_content_types.rb +46 -0
- data/lib/grape/middleware/stack.rb +5 -6
- data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
- data/lib/grape/middleware/versioner/base.rb +34 -38
- data/lib/grape/middleware/versioner/header.rb +3 -5
- data/lib/grape/middleware/versioner/path.rb +8 -3
- data/lib/grape/namespace.rb +3 -3
- data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
- data/lib/grape/parser/json.rb +1 -1
- data/lib/grape/path.rb +14 -17
- data/lib/grape/request.rb +15 -8
- data/lib/grape/router/mustermann_pattern.rb +44 -0
- data/lib/grape/router/pattern.rb +6 -10
- data/lib/grape/router.rb +28 -42
- data/lib/grape/serve_stream/file_body.rb +1 -0
- data/lib/grape/serve_stream/sendfile_response.rb +3 -5
- data/lib/grape/serve_stream/stream_response.rb +1 -0
- data/lib/grape/testing.rb +33 -0
- data/lib/grape/util/base_inheritable.rb +13 -16
- data/lib/grape/util/inheritable_setting.rb +44 -27
- data/lib/grape/util/inheritable_values.rb +7 -3
- data/lib/grape/util/lazy/base.rb +16 -0
- data/lib/grape/util/lazy/block.rb +2 -9
- data/lib/grape/util/lazy/value.rb +2 -9
- data/lib/grape/util/lazy/value_enumerable.rb +13 -16
- data/lib/grape/util/media_type.rb +1 -4
- data/lib/grape/util/path_normalizer.rb +34 -0
- data/lib/grape/util/registry.rb +1 -1
- data/lib/grape/util/stackable_values.rb +11 -8
- data/lib/grape/validations/attributes_iterator.rb +13 -13
- data/lib/grape/validations/coerce_options.rb +21 -0
- data/lib/grape/validations/oneof_collector.rb +39 -0
- data/lib/grape/validations/param_scope_tracker.rb +14 -9
- data/lib/grape/validations/params_documentation.rb +25 -23
- data/lib/grape/validations/params_scope.rb +54 -172
- data/lib/grape/validations/shared_options.rb +19 -0
- data/lib/grape/validations/types/array_coercer.rb +2 -2
- data/lib/grape/validations/types/custom_type_coercer.rb +41 -85
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +3 -3
- data/lib/grape/validations/types/primitive_coercer.rb +10 -5
- data/lib/grape/validations/types/set_coercer.rb +1 -1
- data/lib/grape/validations/types/variant_collection_coercer.rb +8 -0
- data/lib/grape/validations/types.rb +23 -30
- data/lib/grape/validations/validations_spec.rb +149 -0
- data/lib/grape/validations/validators/all_or_none_of_validator.rb +1 -1
- data/lib/grape/validations/validators/at_least_one_of_validator.rb +1 -1
- data/lib/grape/validations/validators/base.rb +39 -22
- data/lib/grape/validations/validators/coerce_validator.rb +5 -3
- data/lib/grape/validations/validators/default_validator.rb +7 -8
- data/lib/grape/validations/validators/except_values_validator.rb +3 -2
- data/lib/grape/validations/validators/length_validator.rb +1 -1
- data/lib/grape/validations/validators/multiple_params_base.rb +10 -7
- data/lib/grape/validations/validators/oneof_validator.rb +49 -0
- data/lib/grape/validations/validators/values_validator.rb +5 -5
- data/lib/grape/version.rb +1 -1
- data/lib/grape/xml.rb +8 -1
- data/lib/grape.rb +6 -6
- metadata +34 -18
- 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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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(
|
|
34
|
-
|
|
35
|
-
formatter = Grape::ErrorFormatter.formatter_for(
|
|
36
|
-
return formatter.call(
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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(
|
|
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
|
|
70
|
-
|
|
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
|
|
78
|
-
error, handler =
|
|
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
|
|
125
|
+
return unless rescue_grape_exceptions || !rescue_all
|
|
89
126
|
|
|
90
|
-
|
|
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
|
|
132
|
+
return unless rescue_all || rescue_grape_exceptions
|
|
96
133
|
|
|
97
|
-
|
|
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.
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 =
|
|
33
|
+
status, headers, bodies = @app_response
|
|
21
34
|
|
|
22
|
-
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
41
|
-
bodymap =
|
|
42
|
-
bodies.
|
|
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
|
|
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,
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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] :
|
|
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,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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'] ||
|
|
127
|
-
if content_type_for(fmt)
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
@@ -4,43 +4,43 @@ module Grape
|
|
|
4
4
|
module Middleware
|
|
5
5
|
module Versioner
|
|
6
6
|
class Base < Grape::Middleware::Base
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
@
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|