grape 4.0.1 → 4.1.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 +102 -1
- data/README.md +195 -95
- data/UPGRADING.md +364 -0
- data/grape.gemspec +1 -1
- data/lib/grape/api/instance.rb +57 -30
- data/lib/grape/api.rb +50 -20
- data/lib/grape/content_types.rb +26 -9
- data/lib/grape/cookies.rb +1 -1
- data/lib/grape/declared_params_handler.rb +35 -7
- data/lib/grape/dsl/declared.rb +5 -4
- data/lib/grape/dsl/desc.rb +1 -2
- data/lib/grape/dsl/entity.rb +35 -19
- data/lib/grape/dsl/headers.rb +3 -3
- data/lib/grape/dsl/helpers.rb +5 -8
- data/lib/grape/dsl/inside_route.rb +33 -27
- data/lib/grape/dsl/logger.rb +10 -4
- data/lib/grape/dsl/middleware.rb +4 -9
- data/lib/grape/dsl/mounting.rb +149 -0
- data/lib/grape/dsl/parameters.rb +78 -104
- data/lib/grape/dsl/request_response.rb +35 -22
- data/lib/grape/dsl/routing.rb +17 -191
- data/lib/grape/dsl/settings.rb +1 -1
- data/lib/grape/dsl/version_options.rb +2 -2
- data/lib/grape/dsl/versioning.rb +98 -0
- data/lib/grape/endpoint/state.rb +15 -0
- data/lib/grape/endpoint.rb +229 -137
- data/lib/grape/error_formatter/base.rb +28 -13
- data/lib/grape/error_formatter/json.rb +3 -0
- data/lib/grape/error_formatter/jsonl.rb +15 -0
- data/lib/grape/error_formatter/xml.rb +3 -1
- data/lib/grape/exceptions/error_response.rb +23 -7
- data/lib/grape/exceptions/halt.rb +39 -0
- data/lib/grape/exceptions/validation.rb +9 -9
- data/lib/grape/exceptions/validation_errors.rb +12 -2
- data/lib/grape/formatter/jsonl.rb +34 -0
- data/lib/grape/formatter/markdown.rb +17 -0
- data/lib/grape/formatter/md.rb +11 -0
- data/lib/grape/locale/en.yml +1 -0
- data/lib/grape/middleware/auth/base.rb +16 -1
- data/lib/grape/middleware/auth/strategy_info.rb +2 -2
- data/lib/grape/middleware/base.rb +19 -13
- data/lib/grape/middleware/error.rb +133 -35
- data/lib/grape/middleware/formatter.rb +235 -94
- data/lib/grape/middleware/precomputed_content_types.rb +14 -2
- data/lib/grape/middleware/stack.rb +7 -8
- data/lib/grape/middleware/versioner/base.rb +26 -10
- data/lib/grape/middleware/versioner/header.rb +70 -15
- data/lib/grape/middleware/versioner/param.rb +3 -5
- data/lib/grape/middleware/versioner/path.rb +20 -11
- data/lib/grape/middleware/versioner.rb +1 -1
- data/lib/grape/mountable.rb +2 -4
- data/lib/grape/namespace.rb +2 -3
- data/lib/grape/parser/jsonl.rb +19 -0
- data/lib/grape/request.rb +46 -8
- data/lib/grape/router/base_route.rb +41 -0
- data/lib/grape/router/greedy_route.rb +1 -1
- data/lib/grape/router/mustermann_pattern.rb +1 -1
- data/lib/grape/router/pattern/path.rb +1 -1
- data/lib/grape/router/pattern.rb +11 -5
- data/lib/grape/router/route.rb +42 -11
- data/lib/grape/router/route_buckets.rb +246 -0
- data/lib/grape/router.rb +262 -32
- data/lib/grape/util/endpoint_configuration.rb +7 -1
- data/lib/grape/util/inheritable_setting.rb +80 -161
- data/lib/grape/util/lazy/value.rb +52 -11
- data/lib/grape/util/media_type.rb +38 -15
- data/lib/grape/util/path_normalizer.rb +7 -2
- data/lib/grape/util/registry.rb +9 -2
- data/lib/grape/util/scrub.rb +33 -0
- data/lib/grape/util/translation.rb +6 -6
- data/lib/grape/validations/attributes_iterator.rb +73 -37
- data/lib/grape/validations/coerce_options.rb +1 -1
- data/lib/grape/validations/params_documentation.rb +15 -7
- data/lib/grape/validations/params_scope.rb +176 -43
- data/lib/grape/validations/types/custom_type_coercer.rb +2 -3
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +20 -12
- data/lib/grape/validations/types/multiple_type_coercer.rb +1 -2
- data/lib/grape/validations/types/primitive_coercer.rb +17 -0
- data/lib/grape/validations/types/variant_collection_coercer.rb +24 -10
- data/lib/grape/validations/types.rb +12 -18
- data/lib/grape/validations/validations_spec.rb +53 -22
- data/lib/grape/validations/validators/allow_blank_validator.rb +2 -2
- data/lib/grape/validations/validators/as_validator.rb +2 -3
- data/lib/grape/validations/validators/base.rb +124 -25
- data/lib/grape/validations/validators/coerce_validator.rb +2 -3
- data/lib/grape/validations/validators/default_validator.rb +18 -2
- data/lib/grape/validations/validators/except_values_validator.rb +16 -3
- data/lib/grape/validations/validators/hash_elements_validator.rb +34 -0
- data/lib/grape/validations/validators/length_validator.rb +24 -16
- data/lib/grape/validations/validators/multiple_params_base.rb +0 -4
- data/lib/grape/validations/validators/values_validator.rb +18 -24
- data/lib/grape/version.rb +1 -1
- data/lib/grape.rb +3 -0
- metadata +18 -11
- data/lib/grape/util/lazy/value_array.rb +0 -21
- data/lib/grape/util/lazy/value_enumerable.rb +0 -31
- data/lib/grape/util/lazy/value_hash.rb +0 -21
- data/lib/grape/validations/multiple_attributes_iterator.rb +0 -13
- data/lib/grape/validations/single_attribute_iterator.rb +0 -28
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Middleware
|
|
5
5
|
class Error < Base
|
|
6
|
-
extend Forwardable
|
|
7
6
|
include PrecomputedContentTypes
|
|
8
7
|
|
|
9
8
|
Options = Data.define(
|
|
@@ -25,30 +24,27 @@ module Grape
|
|
|
25
24
|
# `Endpoint#error_middleware_options` when the API called no
|
|
26
25
|
# `rescue_from` and no `format` — and an explicit nil bypasses the
|
|
27
26
|
# keyword defaults above, so restore them here rather than letting nil
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
default_message:, default_status:, error_formatters:, format:,
|
|
35
|
-
grape_exceptions_rescue_handler:, internal_grape_exceptions_rescue_handler:,
|
|
36
|
-
rescue_all:, rescue_grape_exceptions:, rescue_handlers:
|
|
37
|
-
)
|
|
27
|
+
# reach `rescue_options.backtrace` in `#initialize` or the formatter
|
|
28
|
+
# lookup in `#format_message`. A bare `super` hands the parameters
|
|
29
|
+
# over as they stand, these two included.
|
|
30
|
+
rescue_options ||= Grape::DSL::RescueOptions.new
|
|
31
|
+
default_error_formatter ||= Grape::ErrorFormatter::Txt
|
|
32
|
+
super
|
|
38
33
|
end
|
|
39
34
|
end
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
# Read off ivars rather than delegated into +config+: rendering an error
|
|
37
|
+
# asks for most of them, and each delegator cost a Forwardable frame plus
|
|
38
|
+
# a Data reader for a value that was fixed when the middleware was built.
|
|
39
|
+
attr_reader :all_rescue_handler, :base_only_rescue_handlers, :default_error_formatter,
|
|
40
|
+
:default_message, :default_status, :error_formatters, :format,
|
|
41
|
+
:grape_exceptions_rescue_handler, :internal_grape_exceptions_rescue_handler,
|
|
42
|
+
:rescue_all, :rescue_grape_exceptions, :rescue_handlers, :rescue_options
|
|
46
43
|
|
|
47
44
|
# +:backtrace+ / +:original_exception+ on the rescue options become
|
|
48
45
|
# +#include_backtrace+ / +#include_original_exception+ on the middleware,
|
|
49
46
|
# which is what the formatter call site reads.
|
|
50
|
-
|
|
51
|
-
def_delegator :rescue_options, :original_exception, :include_original_exception
|
|
47
|
+
attr_reader :include_backtrace, :include_original_exception
|
|
52
48
|
|
|
53
49
|
# Emitted by {#render_failsafe_response} once even the framework's own message
|
|
54
50
|
# could not be rendered. Deliberately built without a formatter, an i18n
|
|
@@ -57,18 +53,93 @@ module Grape
|
|
|
57
53
|
FAILSAFE_MESSAGE = '500 Internal Server Error'
|
|
58
54
|
FAILSAFE_CONTENT_TYPE = 'text/plain'
|
|
59
55
|
|
|
56
|
+
def initialize(app, **options)
|
|
57
|
+
super
|
|
58
|
+
read_rescue_config(config)
|
|
59
|
+
read_rendering_config(config)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Base#call copies the middleware for every request so that #call! can
|
|
63
|
+
# keep the env in an ivar, but only rendering an error reads it there. A
|
|
64
|
+
# request that goes through without one is answered by the one instance
|
|
65
|
+
# the stack built, and a copy is taken once there is an error to render.
|
|
66
|
+
def call(env)
|
|
67
|
+
respond(env).to_a
|
|
68
|
+
end
|
|
69
|
+
|
|
60
70
|
def call!(env)
|
|
61
71
|
@env = env
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
respond(env)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
protected
|
|
76
|
+
|
|
77
|
+
# What #respond hands a thrown error or a raised exception to: a copy of
|
|
78
|
+
# the middleware, holding the request's env for the rendering below.
|
|
79
|
+
def render_thrown(env, error)
|
|
80
|
+
@env = env
|
|
81
|
+
error_response(error)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def render_raised(env, exception)
|
|
85
|
+
@env = env
|
|
86
|
+
run_rescue_handler(find_handler(exception.class), exception, env[Grape::Env::API_ENDPOINT])
|
|
65
87
|
end
|
|
66
88
|
|
|
67
89
|
private
|
|
68
90
|
|
|
91
|
+
# What #find_handler looks a rescue handler up in.
|
|
92
|
+
def read_rescue_config(config)
|
|
93
|
+
@all_rescue_handler = config.all_rescue_handler
|
|
94
|
+
@base_only_rescue_handlers = config.base_only_rescue_handlers
|
|
95
|
+
@grape_exceptions_rescue_handler = config.grape_exceptions_rescue_handler
|
|
96
|
+
@internal_grape_exceptions_rescue_handler = config.internal_grape_exceptions_rescue_handler
|
|
97
|
+
@rescue_all = config.rescue_all
|
|
98
|
+
@rescue_grape_exceptions = config.rescue_grape_exceptions
|
|
99
|
+
@rescue_handlers = config.rescue_handlers
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# What an error response is rendered with.
|
|
103
|
+
def read_rendering_config(config)
|
|
104
|
+
@default_error_formatter = config.default_error_formatter
|
|
105
|
+
@default_message = config.default_message
|
|
106
|
+
@default_status = config.default_status
|
|
107
|
+
@error_formatters = config.error_formatters
|
|
108
|
+
@format = config.format
|
|
109
|
+
@rescue_options = config.rescue_options
|
|
110
|
+
@include_backtrace = @rescue_options.backtrace
|
|
111
|
+
@include_original_exception = @rescue_options.original_exception
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Whether the app answered is recorded rather than returned from inside
|
|
115
|
+
# the +catch+ block: a +return+ there leaves the method through Kernel#catch,
|
|
116
|
+
# which unwinds like a throw and allocates for it, on every request that
|
|
117
|
+
# did not fail.
|
|
118
|
+
#
|
|
119
|
+
# +error!+ raises a Halt carrying what a +throw :error+ carries. It is
|
|
120
|
+
# taken inside the +catch+, so it is rendered where a thrown error is and
|
|
121
|
+
# never reaches a +rescue_from+ handler, the +:all+ one included.
|
|
122
|
+
def respond(env)
|
|
123
|
+
answered = false
|
|
124
|
+
response = catch(:error) do
|
|
125
|
+
app_response = @app.call(env)
|
|
126
|
+
answered = true
|
|
127
|
+
app_response
|
|
128
|
+
rescue Grape::Exceptions::Halt => e
|
|
129
|
+
e.response
|
|
130
|
+
end
|
|
131
|
+
answered ? response : dup.render_thrown(env, response)
|
|
132
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
133
|
+
dup.render_raised(env, e)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# +headers+ is handed over as it is: Rack::Response copies it into a
|
|
137
|
+
# Headers of its own on every supported Rack (Rack 2.2 through
|
|
138
|
+
# +HeaderHash[]+, which only adopts a Hash that already is one), and every
|
|
139
|
+
# caller builds it fresh for this response.
|
|
69
140
|
def rack_response(status, headers, message)
|
|
70
141
|
body = html_content_type?(headers[Rack::CONTENT_TYPE]) ? Rack::Utils.escape_html(message) : message
|
|
71
|
-
Rack::Response.new(Array.wrap(body), Rack::Utils.status_code(status),
|
|
142
|
+
Rack::Response.new(Array.wrap(body), Rack::Utils.status_code(status), headers)
|
|
72
143
|
end
|
|
73
144
|
|
|
74
145
|
# Escaping must key off the media type only, case-insensitively. Comparing
|
|
@@ -87,7 +158,7 @@ module Grape
|
|
|
87
158
|
# API set another — takes it from there, so this always has something
|
|
88
159
|
# callable. The +throw :error, 406+ that used to stand in for a missing
|
|
89
160
|
# formatter could not work anyway: nothing catches +:error+ around this
|
|
90
|
-
# call (+#
|
|
161
|
+
# call (+#respond+ has left its +catch+ by the time +error_response+ runs),
|
|
91
162
|
# so it raised +UncaughtThrowError+ and the request answered with the
|
|
92
163
|
# failsafe 500 rather than the 406 it named.
|
|
93
164
|
def format_message(error)
|
|
@@ -108,18 +179,25 @@ module Grape
|
|
|
108
179
|
|
|
109
180
|
def error_response(error = nil)
|
|
110
181
|
raw = Grape::Exceptions::ErrorResponse.coerce(error)
|
|
111
|
-
|
|
112
|
-
headers.merge!(raw.headers) if raw.headers.is_a?(Hash)
|
|
113
|
-
payload = raw.with(
|
|
182
|
+
payload = Grape::Exceptions::ErrorResponse.new(
|
|
114
183
|
status: raw.status || default_status,
|
|
115
184
|
message: raw.message || default_message,
|
|
116
|
-
headers
|
|
117
|
-
backtrace: resolved_backtrace(raw)
|
|
185
|
+
headers: error_response_headers(raw.headers),
|
|
186
|
+
backtrace: resolved_backtrace(raw),
|
|
187
|
+
original_exception: raw.original_exception
|
|
118
188
|
)
|
|
119
189
|
env[Grape::Env::API_ENDPOINT].status(payload.status) # error! may not have been called
|
|
120
190
|
render_response(payload)
|
|
121
191
|
end
|
|
122
192
|
|
|
193
|
+
# The content type the error renders as, under any +headers+ it carries.
|
|
194
|
+
def error_response_headers(headers)
|
|
195
|
+
content_type_header = { Rack::CONTENT_TYPE => content_type }
|
|
196
|
+
return content_type_header unless headers.is_a?(Hash)
|
|
197
|
+
|
|
198
|
+
content_type_header.merge!(headers)
|
|
199
|
+
end
|
|
200
|
+
|
|
123
201
|
# The backtrace an error formatter is handed. Assembled only when the API
|
|
124
202
|
# asked for one with `rescue_from ..., backtrace: true`: reading it off
|
|
125
203
|
# the exception is not free — +Exception#backtrace+ builds the whole Array
|
|
@@ -131,11 +209,11 @@ module Grape
|
|
|
131
209
|
raw.backtrace || raw.original_exception&.backtrace || []
|
|
132
210
|
end
|
|
133
211
|
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
212
|
+
# An error formatter that raises on its payload would take the exception
|
|
213
|
+
# out through every middleware above Grape: rendering from a rescue
|
|
214
|
+
# handler runs in #respond's rescue clause, which nothing covers. Grape has
|
|
215
|
+
# committed to an error by now, so it answers with one that does not
|
|
216
|
+
# depend on the payload.
|
|
139
217
|
#
|
|
140
218
|
# +Grape.config.raise_rendering_errors+ opts back out, for an application
|
|
141
219
|
# that would rather have the exception propagate as it did before.
|
|
@@ -192,11 +270,13 @@ module Grape
|
|
|
192
270
|
)
|
|
193
271
|
end
|
|
194
272
|
|
|
273
|
+
# No +backtrace:+: #resolved_backtrace reads it off +original_exception+
|
|
274
|
+
# when the API asked for one, and only then, since building it is the
|
|
275
|
+
# dearest part of rendering the error.
|
|
195
276
|
def default_rescue_handler(exception)
|
|
196
277
|
error_response(
|
|
197
278
|
Grape::Exceptions::ErrorResponse.new(
|
|
198
279
|
message: exception.message,
|
|
199
|
-
backtrace: exception.backtrace,
|
|
200
280
|
original_exception: exception
|
|
201
281
|
)
|
|
202
282
|
)
|
|
@@ -268,8 +348,12 @@ module Grape
|
|
|
268
348
|
def run_rescue_handler(handler, error, endpoint, redispatched: false)
|
|
269
349
|
callable = handler.is_a?(Symbol) ? endpoint.public_method(handler) : handler
|
|
270
350
|
response = catch(:error) do
|
|
271
|
-
|
|
351
|
+
call_rescue_handler(callable, error, endpoint)
|
|
352
|
+
rescue Grape::Exceptions::Halt => e
|
|
353
|
+
e.response
|
|
272
354
|
rescue StandardError => e
|
|
355
|
+
raise if Grape.config.raise_rescue_handler_errors
|
|
356
|
+
|
|
273
357
|
return redispatch(e, endpoint, redispatched)
|
|
274
358
|
end
|
|
275
359
|
|
|
@@ -279,6 +363,20 @@ module Grape
|
|
|
279
363
|
run_rescue_handler(method(:default_rescue_handler), Grape::Exceptions::InvalidResponse.new, endpoint)
|
|
280
364
|
end
|
|
281
365
|
|
|
366
|
+
# A +rescue_from+ block runs as the endpoint. A Method (the middleware's
|
|
367
|
+
# own handlers, or the endpoint's for a +with:+ Symbol) is already bound
|
|
368
|
+
# to a receiver that instance_exec cannot change, so it is called as it
|
|
369
|
+
# is rather than turned into a Proc first.
|
|
370
|
+
def call_rescue_handler(callable, error, endpoint)
|
|
371
|
+
if callable.is_a?(Method)
|
|
372
|
+
callable.arity.zero? ? callable.call : callable.call(error)
|
|
373
|
+
elsif callable.arity.zero?
|
|
374
|
+
endpoint.instance_exec(&callable)
|
|
375
|
+
else
|
|
376
|
+
endpoint.instance_exec(error, &callable)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
282
380
|
# Route an exception raised inside a +rescue_from+ block.
|
|
283
381
|
#
|
|
284
382
|
# * If we have already redispatched once (the redispatched handler
|