grape 3.3.4 → 4.0.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 +128 -0
- data/CONTRIBUTING.md +16 -0
- data/README.md +208 -8
- data/UPGRADING.md +756 -6
- data/lib/grape/api/instance.rb +40 -33
- data/lib/grape/content_types.rb +47 -1
- data/lib/grape/dry_types.rb +1 -1
- data/lib/grape/dsl/callbacks.rb +3 -9
- data/lib/grape/dsl/declared.rb +4 -4
- data/lib/grape/dsl/desc.rb +18 -8
- data/lib/grape/dsl/entity.rb +37 -19
- data/lib/grape/dsl/helpers.rb +3 -3
- data/lib/grape/dsl/inside_route.rb +20 -8
- data/lib/grape/dsl/middleware.rb +3 -3
- data/lib/grape/dsl/parameters.rb +54 -28
- data/lib/grape/dsl/request_response.rb +37 -39
- data/lib/grape/dsl/rescue_options.rb +6 -5
- data/lib/grape/dsl/routing.rb +116 -49
- data/lib/grape/dsl/settings.rb +1 -1
- data/lib/grape/dsl/validations.rb +3 -3
- data/lib/grape/dsl/version_options.rb +3 -3
- data/lib/grape/endpoint/options.rb +13 -7
- data/lib/grape/endpoint.rb +141 -118
- data/lib/grape/env.rb +7 -0
- data/lib/grape/error_formatter/base.rb +6 -5
- data/lib/grape/error_formatter.rb +6 -2
- data/lib/grape/exceptions/error_response.rb +4 -1
- data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
- data/lib/grape/exceptions/validation.rb +9 -8
- data/lib/grape/formatter/json.rb +1 -0
- data/lib/grape/formatter/serializable_hash.rb +1 -0
- data/lib/grape/locale/en.yml +1 -0
- data/lib/grape/middleware/auth/dsl.rb +5 -33
- data/lib/grape/middleware/error.rb +153 -46
- data/lib/grape/middleware/formatter.rb +95 -26
- data/lib/grape/middleware/precomputed_content_types.rb +12 -7
- data/lib/grape/middleware/stack.rb +13 -4
- data/lib/grape/middleware/versioner/base.rb +3 -9
- data/lib/grape/middleware/versioner/path.rb +49 -7
- data/lib/grape/namespace.rb +3 -2
- data/lib/grape/path.rb +8 -69
- data/lib/grape/precompiled_json.rb +50 -0
- data/lib/grape/request.rb +36 -4
- data/lib/grape/router/base_route.rb +47 -10
- data/lib/grape/router/greedy_route.rb +5 -1
- data/lib/grape/router/pattern/path.rb +78 -0
- data/lib/grape/router/pattern.rb +60 -14
- data/lib/grape/router/route.rb +62 -10
- data/lib/grape/router.rb +100 -57
- data/lib/grape/serve_stream/file_body.rb +6 -0
- data/lib/grape/serve_stream/stream_response.rb +6 -0
- data/lib/grape/util/api_description.rb +10 -1
- data/lib/grape/util/cache.rb +21 -2
- data/lib/grape/util/deep_freeze.rb +1 -2
- data/lib/grape/util/freeze_on_new.rb +20 -0
- data/lib/grape/util/inheritable_setting.rb +751 -37
- data/lib/grape/util/media_type.rb +10 -3
- data/lib/grape/util/path_normalizer.rb +12 -9
- data/lib/grape/util/registry.rb +12 -2
- data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
- data/lib/grape/util/stackable_values.rb +32 -6
- data/lib/grape/validations/attributes_iterator.rb +21 -4
- data/lib/grape/validations/contract_scope.rb +8 -7
- data/lib/grape/validations/oneof_collector.rb +6 -10
- data/lib/grape/validations/params_documentation.rb +2 -2
- data/lib/grape/validations/params_scope.rb +66 -26
- data/lib/grape/validations/single_attribute_iterator.rb +5 -3
- data/lib/grape/validations/types/array_coercer.rb +4 -6
- data/lib/grape/validations/types/custom_type_coercer.rb +7 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +3 -1
- data/lib/grape/validations/types/json.rb +1 -3
- data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
- data/lib/grape/validations/types/primitive_coercer.rb +12 -9
- data/lib/grape/validations/types/variant_collection_coercer.rb +8 -3
- data/lib/grape/validations/types.rb +5 -5
- data/lib/grape/validations/validations_spec.rb +14 -4
- data/lib/grape/validations/validators/base.rb +4 -7
- data/lib/grape/validations/validators/coerce_validator.rb +8 -2
- data/lib/grape/validations/validators/contract_scope_validator.rb +2 -1
- data/lib/grape/validations/validators/length_validator.rb +4 -2
- data/lib/grape/validations/validators/oneof_validator.rb +2 -0
- data/lib/grape/validations/validators/same_as_validator.rb +1 -0
- data/lib/grape/version.rb +2 -1
- data/lib/grape.rb +23 -2
- metadata +10 -9
- data/lib/grape/middleware/deprecated_options_hash_access.rb +0 -19
- data/lib/grape/util/base_inheritable.rb +0 -43
- data/lib/grape/util/inheritable_values.rb +0 -33
- data/lib/grape/util/reverse_stackable_values.rb +0 -15
data/lib/grape/endpoint.rb
CHANGED
|
@@ -17,11 +17,19 @@ module Grape
|
|
|
17
17
|
def_delegators :request, :params, :headers, :cookies
|
|
18
18
|
def_delegator :cookies, :response_cookies
|
|
19
19
|
|
|
20
|
+
# The API (a +Grape::API+ instance) this endpoint belongs to.
|
|
21
|
+
def_delegator :@config, :api
|
|
22
|
+
|
|
20
23
|
# The logger configured on the API this endpoint belongs to. Available
|
|
21
24
|
# inside route handlers, +before+/+after+/+after_validation+/+finally+
|
|
22
25
|
# filters, and +rescue_from+ blocks.
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
def_delegator :api, :logger
|
|
27
|
+
|
|
28
|
+
# The Rack app or Grape API mounted at this endpoint, or +nil+ for a plain
|
|
29
|
+
# block endpoint. Prefer this over +options[:app]+, which is retained only
|
|
30
|
+
# for backwards compatibility.
|
|
31
|
+
def mounted_app
|
|
32
|
+
config.app
|
|
25
33
|
end
|
|
26
34
|
|
|
27
35
|
class << self
|
|
@@ -38,33 +46,34 @@ module Grape
|
|
|
38
46
|
# Create a new endpoint.
|
|
39
47
|
# @param new_settings [InheritableSetting] settings to determine the params,
|
|
40
48
|
# validations, and other properties from.
|
|
49
|
+
# @param http_methods [String or Array] which HTTP method(s) can be used to
|
|
50
|
+
# reach this endpoint.
|
|
51
|
+
# @param path [String or Array] the path to this endpoint, within the
|
|
52
|
+
# current scope.
|
|
53
|
+
# @param api [Grape::API] the API this endpoint belongs to. Exposed as
|
|
54
|
+
# {#api}.
|
|
55
|
+
# @param app [#call, nil] the Rack app or Grape API mounted at this
|
|
56
|
+
# endpoint; +nil+ for a plain block endpoint. Exposed as {#mounted_app}.
|
|
57
|
+
# @param params [Hash] the declared params for this endpoint, keyed by name.
|
|
58
|
+
# Kept out of +route_options+ and read via +config.params+.
|
|
59
|
+
# @param requirements [Hash, nil] regular-expression constraints for named
|
|
60
|
+
# path params. Read via +config.requirements+.
|
|
61
|
+
# @param anchor [Boolean] whether the route anchors to the whole path
|
|
62
|
+
# (default +true+). Read via +config.anchor+.
|
|
41
63
|
# @param options [Hash] attributes of this endpoint, normalized into a
|
|
42
64
|
# +Grape::Endpoint::Options+ value object.
|
|
43
|
-
# @option options path [String or Array] the path to this endpoint, within
|
|
44
|
-
# the current scope.
|
|
45
|
-
# @option options method [String or Array] which HTTP method(s) can be used
|
|
46
|
-
# to reach this endpoint.
|
|
47
65
|
# @option options route_options [Hash]
|
|
48
66
|
# @note This happens at the time of API definition, so in this context the
|
|
49
67
|
# endpoint does not know if it will be mounted under a different endpoint.
|
|
50
68
|
# @yield a block defining what your API should do when this endpoint is hit
|
|
51
|
-
def initialize(new_settings, **options, &block)
|
|
52
|
-
self.inheritable_setting = new_settings.
|
|
53
|
-
|
|
54
|
-
# now +namespace_stackable(:declared_params)+ contains all params defined for
|
|
55
|
-
# this endpoint and its parents, but later it will be cleaned up,
|
|
56
|
-
# see +reset_validations!+ in lib/grape/dsl/validations.rb
|
|
57
|
-
inheritable_setting.route[:declared_params] = inheritable_setting.namespace_stackable[:declared_params].flatten
|
|
58
|
-
inheritable_setting.route[:saved_validations] = inheritable_setting.namespace_stackable[:validations].dup
|
|
59
|
-
|
|
60
|
-
inheritable_setting.namespace_stackable[:representations] ||= []
|
|
61
|
-
inheritable_setting.namespace_inheritable[:default_error_status] ||= 500
|
|
69
|
+
def initialize(new_settings, http_methods:, path:, api:, app: nil, params: {}, requirements: nil, anchor: true, **options, &block)
|
|
70
|
+
self.inheritable_setting = new_settings.point_in_time_copy_for_endpoint
|
|
62
71
|
|
|
63
72
|
@options = options
|
|
64
|
-
@
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@
|
|
73
|
+
@config = Options.new(http_methods:, path:, api:, app:, params:, requirements:, anchor:, **options)
|
|
74
|
+
# +:app+ is still surfaced on the public options Hash for backwards
|
|
75
|
+
# compatibility (e.g. grape-swagger); prefer the +mounted_app+ reader.
|
|
76
|
+
@options[:app] = app if app
|
|
68
77
|
|
|
69
78
|
@status = nil
|
|
70
79
|
@stream = nil
|
|
@@ -75,15 +84,12 @@ module Grape
|
|
|
75
84
|
@endpoints = @config.app.endpoints if @config.app.respond_to?(:endpoints)
|
|
76
85
|
end
|
|
77
86
|
|
|
78
|
-
# Update our settings from a given
|
|
87
|
+
# Update our settings from a given parent settings instance. Used when
|
|
79
88
|
# the endpoint's API is mounted under another one.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
inheritable_setting.
|
|
83
|
-
|
|
84
|
-
inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten) if parent_declared_params.any?
|
|
85
|
-
|
|
86
|
-
endpoints&.each { |e| e.inherit_settings(namespace_stackable) }
|
|
89
|
+
# @param settings [Grape::Util::InheritableSetting]
|
|
90
|
+
def inherit_settings(settings)
|
|
91
|
+
inheritable_setting.inherit_route_params(settings)
|
|
92
|
+
endpoints&.each { |e| e.inherit_settings(settings) }
|
|
87
93
|
end
|
|
88
94
|
|
|
89
95
|
def routes
|
|
@@ -106,28 +112,23 @@ module Grape
|
|
|
106
112
|
compile!
|
|
107
113
|
routes.each do |route|
|
|
108
114
|
router.append(route.apply(self))
|
|
109
|
-
next if inheritable_setting.
|
|
115
|
+
next if inheritable_setting.do_not_route_head? || route.request_method != Rack::GET
|
|
110
116
|
|
|
111
|
-
route.
|
|
112
|
-
head_route.convert_to_head_request!
|
|
113
|
-
router.append(head_route.apply(self))
|
|
114
|
-
end
|
|
117
|
+
router.append(route.to_head.apply(self))
|
|
115
118
|
end
|
|
116
119
|
end
|
|
117
120
|
|
|
118
121
|
def namespace
|
|
119
|
-
@namespace ||=
|
|
122
|
+
@namespace ||= inheritable_setting.namespace_path
|
|
120
123
|
end
|
|
121
124
|
|
|
122
125
|
def call(env)
|
|
123
|
-
dup.call!(env)
|
|
126
|
+
@prototype.dup.call!(env)
|
|
124
127
|
end
|
|
125
128
|
|
|
126
129
|
def call!(env)
|
|
127
130
|
env[Grape::Env::API_ENDPOINT] = self
|
|
128
131
|
@env = env
|
|
129
|
-
# this adds the helpers only to the instance
|
|
130
|
-
singleton_class.include(@helpers) if @helpers
|
|
131
132
|
@app.call(env)
|
|
132
133
|
end
|
|
133
134
|
|
|
@@ -138,6 +139,14 @@ module Grape
|
|
|
138
139
|
end
|
|
139
140
|
alias eql? ==
|
|
140
141
|
|
|
142
|
+
# Mirrors #==. The class stays out: #== admits a subclass instance
|
|
143
|
+
# through is_a?, and such a pair must hash alike. The block (#source) is
|
|
144
|
+
# not part of either, matching the long-standing duplicate-route check in
|
|
145
|
+
# DSL::Routing#route.
|
|
146
|
+
def hash
|
|
147
|
+
[config, inheritable_setting].hash
|
|
148
|
+
end
|
|
149
|
+
|
|
141
150
|
# The purpose of this override is solely for stripping internals when an error occurs while calling
|
|
142
151
|
# an endpoint through an api. See https://github.com/ruby-grape/grape/issues/2398
|
|
143
152
|
# Otherwise, it calls super.
|
|
@@ -194,7 +203,7 @@ module Grape
|
|
|
194
203
|
end
|
|
195
204
|
|
|
196
205
|
def run_validators(request:)
|
|
197
|
-
validators = inheritable_setting.
|
|
206
|
+
validators = inheritable_setting.route_validations
|
|
198
207
|
return if validators.blank?
|
|
199
208
|
|
|
200
209
|
validation_exceptions = nil
|
|
@@ -263,34 +272,80 @@ module Grape
|
|
|
263
272
|
@app = config.app || build_stack
|
|
264
273
|
warn_unauthenticated_mounted_app
|
|
265
274
|
@helpers = build_helpers
|
|
266
|
-
|
|
267
|
-
@befores =
|
|
268
|
-
@before_validations =
|
|
269
|
-
@after_validations =
|
|
270
|
-
@afters =
|
|
271
|
-
@finallies =
|
|
272
|
-
@build_params_with = inheritable_setting.
|
|
275
|
+
callbacks = inheritable_setting.callbacks
|
|
276
|
+
@befores = callbacks.fetch(:before)
|
|
277
|
+
@before_validations = callbacks.fetch(:before_validation)
|
|
278
|
+
@after_validations = callbacks.fetch(:after_validation)
|
|
279
|
+
@afters = callbacks.fetch(:after)
|
|
280
|
+
@finallies = callbacks.fetch(:finally)
|
|
281
|
+
@build_params_with = inheritable_setting.build_params_with
|
|
282
|
+
@prototype = build_prototype
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# The object each request is copied from.
|
|
286
|
+
#
|
|
287
|
+
# Helpers reach an endpoint by module inclusion, and +Object#dup+ does not
|
|
288
|
+
# carry a singleton class over — so including them on the per-request copy
|
|
289
|
+
# meant giving every request a brand-new singleton class, whose method
|
|
290
|
+
# cache then started cold for the helpers, the route block and every DSL
|
|
291
|
+
# method the endpoint answers. Include them once into a subclass owned by
|
|
292
|
+
# this endpoint instead, and copy this endpoint's state onto an instance of
|
|
293
|
+
# it: the per-request copy already answers to the helpers, against a class
|
|
294
|
+
# that has been warm since boot. It also keeps request handling free of
|
|
295
|
+
# class mutation.
|
|
296
|
+
#
|
|
297
|
+
# Endpoints with no helpers are their own prototype, exactly as before.
|
|
298
|
+
#
|
|
299
|
+
# +allocate+ rather than +new+ because the constructor rebuilds state from
|
|
300
|
+
# the DSL's arguments — a fresh settings copy, a fresh +@config+, another
|
|
301
|
+
# +block_to_unbound_method+ — and would still not hold what +compile!+ has
|
|
302
|
+
# just computed. What is wanted is what +dup+ itself does (allocate, then
|
|
303
|
+
# copy the ivars over) across a class boundary, which neither +dup+ nor
|
|
304
|
+
# +initialize_copy+ will do: both insist on the receiver's own class. So
|
|
305
|
+
# the copy is written out here. Taking all of them is safe because
|
|
306
|
+
# everything set past this point (+routes+, +namespace+) is route-building
|
|
307
|
+
# memoization the request path never reads.
|
|
308
|
+
def build_prototype
|
|
309
|
+
return self unless @helpers
|
|
310
|
+
|
|
311
|
+
klass = Class.new(self.class)
|
|
312
|
+
# Anonymous classes answer nil to +name+ and render as a bare address,
|
|
313
|
+
# which would reach anything that reports on the endpoint — +inspect+
|
|
314
|
+
# here, a logger or an error tracker elsewhere. The annotation is what
|
|
315
|
+
# +set_temporary_name+ is for; a plain constant path is rejected, which
|
|
316
|
+
# is right, since this class is not reachable under that name.
|
|
317
|
+
klass.set_temporary_name("#{self.class}(helpers)")
|
|
318
|
+
klass.include(@helpers)
|
|
319
|
+
prototype = klass.allocate
|
|
320
|
+
# Everything but the prototype slot itself, so that recompiling (a
|
|
321
|
+
# remount) does not leave the new prototype holding the one it replaces.
|
|
322
|
+
(instance_variables - [:@prototype]).each { |name| prototype.instance_variable_set(name, instance_variable_get(name)) }
|
|
323
|
+
prototype
|
|
273
324
|
end
|
|
274
325
|
|
|
275
326
|
def to_routes
|
|
276
327
|
route_options = config.route_options
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
328
|
+
params = config.params
|
|
329
|
+
path_settings = inheritable_setting.path_settings
|
|
330
|
+
forward_match = bare_rack_app?
|
|
331
|
+
version = prepare_version(inheritable_setting.version)
|
|
332
|
+
prefix = inheritable_setting.root_prefix
|
|
333
|
+
requirements = prepare_routes_requirements(config.requirements)
|
|
334
|
+
anchor = config.anchor
|
|
335
|
+
settings = inheritable_setting.route_settings
|
|
280
336
|
|
|
281
337
|
config.http_methods.flat_map do |method|
|
|
282
338
|
config.path.map do |path|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
anchor
|
|
288
|
-
params
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
requirements: default_route_options[:requirements]
|
|
339
|
+
pattern = Grape::Router::Pattern.build(
|
|
340
|
+
path:,
|
|
341
|
+
namespace:,
|
|
342
|
+
settings: path_settings,
|
|
343
|
+
anchor:,
|
|
344
|
+
params:,
|
|
345
|
+
version:,
|
|
346
|
+
requirements:
|
|
292
347
|
)
|
|
293
|
-
Grape::Router::Route.new(self, method, pattern,
|
|
348
|
+
Grape::Router::Route.new(self, method, pattern, route_options, forward_match:, params:, namespace:, prefix:, settings:)
|
|
294
349
|
end
|
|
295
350
|
end
|
|
296
351
|
end
|
|
@@ -302,26 +357,8 @@ module Grape
|
|
|
302
357
|
config.app && !config.app.is_a?(Grape::Mountable)
|
|
303
358
|
end
|
|
304
359
|
|
|
305
|
-
def prepare_default_route_attributes(route_options)
|
|
306
|
-
{
|
|
307
|
-
namespace:,
|
|
308
|
-
version: prepare_version(inheritable_setting.namespace_inheritable[:version]),
|
|
309
|
-
requirements: prepare_routes_requirements(route_options[:requirements]),
|
|
310
|
-
prefix: inheritable_setting.namespace_inheritable[:root_prefix],
|
|
311
|
-
anchor: route_options.fetch(:anchor, true),
|
|
312
|
-
settings: inheritable_setting.route.except(:declared_params, :saved_validations),
|
|
313
|
-
forward_match: config.forward_match
|
|
314
|
-
}
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
def prepare_default_path_settings
|
|
318
|
-
namespace_stackable_hash = inheritable_setting.namespace_stackable.to_hash
|
|
319
|
-
namespace_inheritable_hash = inheritable_setting.namespace_inheritable.to_hash
|
|
320
|
-
namespace_stackable_hash.merge!(namespace_inheritable_hash)
|
|
321
|
-
end
|
|
322
|
-
|
|
323
360
|
def prepare_routes_requirements(route_options_requirements)
|
|
324
|
-
namespace_requirements = inheritable_setting.
|
|
361
|
+
namespace_requirements = inheritable_setting.namespace_requirements
|
|
325
362
|
namespace_requirements << route_options_requirements if route_options_requirements.present?
|
|
326
363
|
namespace_requirements.reduce({}, :merge)
|
|
327
364
|
end
|
|
@@ -335,30 +372,30 @@ module Grape
|
|
|
335
372
|
def build_stack
|
|
336
373
|
stack = Grape::Middleware::Stack.new
|
|
337
374
|
|
|
338
|
-
content_types = inheritable_setting.
|
|
339
|
-
format = inheritable_setting.
|
|
375
|
+
content_types = inheritable_setting.content_types
|
|
376
|
+
format = inheritable_setting.format
|
|
340
377
|
|
|
341
378
|
stack.use Rack::Head
|
|
342
379
|
stack.use Rack::Lint if lint?
|
|
343
380
|
stack.use Grape::Middleware::Error, **error_middleware_options(format, content_types)
|
|
344
381
|
|
|
345
|
-
stack.concat inheritable_setting.
|
|
382
|
+
stack.concat inheritable_setting.middleware
|
|
346
383
|
|
|
347
|
-
if inheritable_setting.
|
|
348
|
-
version_options = inheritable_setting.
|
|
384
|
+
if inheritable_setting.version.present?
|
|
385
|
+
version_options = inheritable_setting.version_options
|
|
349
386
|
stack.use Grape::Middleware::Versioner.using(version_options.using),
|
|
350
|
-
versions: inheritable_setting.
|
|
387
|
+
versions: inheritable_setting.version.flatten,
|
|
351
388
|
version_options:,
|
|
352
|
-
prefix: inheritable_setting.
|
|
353
|
-
mount_path: inheritable_setting.
|
|
389
|
+
prefix: inheritable_setting.root_prefix,
|
|
390
|
+
mount_path: inheritable_setting.mount_path
|
|
354
391
|
end
|
|
355
392
|
|
|
356
393
|
stack.use Grape::Middleware::Formatter,
|
|
357
394
|
format:,
|
|
358
|
-
default_format: inheritable_setting.
|
|
395
|
+
default_format: inheritable_setting.default_format || :txt,
|
|
359
396
|
content_types:,
|
|
360
|
-
formatters: inheritable_setting.
|
|
361
|
-
parsers: inheritable_setting.
|
|
397
|
+
formatters: inheritable_setting.formatters,
|
|
398
|
+
parsers: inheritable_setting.parsers
|
|
362
399
|
|
|
363
400
|
builder = stack.build
|
|
364
401
|
builder.run ->(env) { env[Grape::Env::API_ENDPOINT].run }
|
|
@@ -366,27 +403,26 @@ module Grape
|
|
|
366
403
|
end
|
|
367
404
|
|
|
368
405
|
def error_middleware_options(format, content_types)
|
|
369
|
-
|
|
370
|
-
ns_stack = inheritable_setting
|
|
406
|
+
setting = inheritable_setting
|
|
371
407
|
{
|
|
372
408
|
format:,
|
|
373
409
|
content_types:,
|
|
374
|
-
default_status:
|
|
375
|
-
rescue_all:
|
|
376
|
-
rescue_grape_exceptions:
|
|
377
|
-
default_error_formatter:
|
|
378
|
-
error_formatters:
|
|
379
|
-
rescue_options:
|
|
380
|
-
rescue_handlers:
|
|
381
|
-
base_only_rescue_handlers:
|
|
382
|
-
all_rescue_handler:
|
|
383
|
-
grape_exceptions_rescue_handler:
|
|
384
|
-
internal_grape_exceptions_rescue_handler:
|
|
410
|
+
default_status: setting.default_error_status,
|
|
411
|
+
rescue_all: setting.rescue_all?,
|
|
412
|
+
rescue_grape_exceptions: setting.rescue_grape_exceptions?,
|
|
413
|
+
default_error_formatter: setting.default_error_formatter,
|
|
414
|
+
error_formatters: setting.error_formatters,
|
|
415
|
+
rescue_options: setting.rescue_options,
|
|
416
|
+
rescue_handlers: setting.rescue_handlers,
|
|
417
|
+
base_only_rescue_handlers: setting.base_only_rescue_handlers,
|
|
418
|
+
all_rescue_handler: setting.all_rescue_handler,
|
|
419
|
+
grape_exceptions_rescue_handler: setting.grape_exceptions_rescue_handler,
|
|
420
|
+
internal_grape_exceptions_rescue_handler: setting.internal_grape_exceptions_rescue_handler
|
|
385
421
|
}
|
|
386
422
|
end
|
|
387
423
|
|
|
388
424
|
def build_helpers
|
|
389
|
-
helpers = inheritable_setting.
|
|
425
|
+
helpers = inheritable_setting.helpers
|
|
390
426
|
return if helpers.empty?
|
|
391
427
|
|
|
392
428
|
Module.new { helpers.each { |mod_to_include| include mod_to_include } }
|
|
@@ -399,7 +435,7 @@ module Grape
|
|
|
399
435
|
# inherited settings. Warn so this bypass isn't silent.
|
|
400
436
|
def warn_unauthenticated_mounted_app
|
|
401
437
|
return unless bare_rack_app?
|
|
402
|
-
return unless inheritable_setting.
|
|
438
|
+
return unless inheritable_setting.auth
|
|
403
439
|
|
|
404
440
|
warn "Grape: #{config.app} is mounted under an API that declares authentication, but authentication " \
|
|
405
441
|
'middleware does not wrap mounted Rack applications. Requests to this mount are not authenticated by Grape.'
|
|
@@ -415,20 +451,7 @@ module Grape
|
|
|
415
451
|
end
|
|
416
452
|
|
|
417
453
|
def lint?
|
|
418
|
-
inheritable_setting.
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
# Merge a reverse-stackable handler map (as written by +rescue_from+) into a
|
|
422
|
-
# single Hash. The reverse store lists child-scope handlers before inherited
|
|
423
|
-
# ones, and the first-wins merge keeps the child's handler for a given
|
|
424
|
-
# class, so a nested +rescue_from+ overrides an outer one.
|
|
425
|
-
def merged_reverse_stackable(key)
|
|
426
|
-
handlers = inheritable_setting.namespace_reverse_stackable[key]
|
|
427
|
-
return if handlers.blank?
|
|
428
|
-
|
|
429
|
-
handlers.each_with_object({}) do |handler, result|
|
|
430
|
-
result.merge!(handler) { |_k, s1, _s2| s1 }
|
|
431
|
-
end
|
|
454
|
+
inheritable_setting.lint? || Grape.config.lint
|
|
432
455
|
end
|
|
433
456
|
end
|
|
434
457
|
end
|
data/lib/grape/env.rb
CHANGED
|
@@ -11,8 +11,15 @@ module Grape
|
|
|
11
11
|
API_VENDOR = 'api.vendor'
|
|
12
12
|
API_FORMAT = 'api.format'
|
|
13
13
|
|
|
14
|
+
GRAPE_NORMALIZED_PATH = 'grape.normalized_path'
|
|
14
15
|
GRAPE_ROUTING_ARGS = 'grape.routing_args'
|
|
15
16
|
GRAPE_ALLOWED_METHODS = 'grape.allowed_methods'
|
|
16
17
|
GRAPE_EXCEPTION = 'grape.exception'
|
|
18
|
+
|
|
19
|
+
# Not a Grape-owned key: the de-facto convention for an exception that was
|
|
20
|
+
# handled rather than raised, which is how error trackers find one they
|
|
21
|
+
# never saw propagate. sentry-ruby, for one, collects
|
|
22
|
+
# +env['rack.exception'] || env['sinatra.error']+.
|
|
23
|
+
RACK_EXCEPTION = 'rack.exception'
|
|
17
24
|
end
|
|
18
25
|
end
|
|
@@ -26,12 +26,13 @@ module Grape
|
|
|
26
26
|
# Extract it here so the presenter can be resolved and the key is not serialized in the response.
|
|
27
27
|
# See spec/integration/grape_entity/entity_spec.rb for examples.
|
|
28
28
|
with = nil
|
|
29
|
+
payload = message
|
|
29
30
|
if message.is_a?(Hash) && message.key?(:with)
|
|
30
|
-
|
|
31
|
-
with =
|
|
31
|
+
payload = message.dup
|
|
32
|
+
with = payload.delete(:with)
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
presenter = with || env[Grape::Env::API_ENDPOINT].entity_class_for_obj(
|
|
35
|
+
presenter = with || env[Grape::Env::API_ENDPOINT].entity_class_for_obj(payload)
|
|
35
36
|
|
|
36
37
|
unless presenter || env[Grape::Env::GRAPE_ROUTING_ARGS].nil?
|
|
37
38
|
# env['api.endpoint'].route does not work when the error occurs within a middleware
|
|
@@ -44,11 +45,11 @@ module Grape
|
|
|
44
45
|
presenter = found_code[2] if found_code
|
|
45
46
|
end
|
|
46
47
|
|
|
47
|
-
return
|
|
48
|
+
return payload unless presenter
|
|
48
49
|
|
|
49
50
|
embeds = { env: }
|
|
50
51
|
embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
|
|
51
|
-
presenter.represent(
|
|
52
|
+
presenter.represent(payload, embeds).serializable_hash
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
def wrap_message(message)
|
|
@@ -6,10 +6,14 @@ module Grape
|
|
|
6
6
|
|
|
7
7
|
module_function
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
# Answers nil when nothing is registered for the format, the way
|
|
10
|
+
# +Parser.parser_for+ does. What to fall back to then is the API's own
|
|
11
|
+
# +default_error_formatter+, which is the caller's state rather than the
|
|
12
|
+
# registry's: +Middleware::Error+ holds it and applies it.
|
|
13
|
+
def formatter_for(format, error_formatters = nil)
|
|
10
14
|
return error_formatters[format] if error_formatters&.key?(format)
|
|
11
15
|
|
|
12
|
-
registry[format]
|
|
16
|
+
registry[format]
|
|
13
17
|
end
|
|
14
18
|
end
|
|
15
19
|
end
|
|
@@ -15,12 +15,15 @@ module Grape
|
|
|
15
15
|
"#<#{self.class.name} status=#{status.inspect} message=#{message.inspect} headers=#{headers.inspect}>"
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# The backtrace is deliberately left unset: +Exception#backtrace+ builds
|
|
19
|
+
# the whole Array of location strings, and the response only renders one
|
|
20
|
+
# when the API asked for it. The exception travels along, so
|
|
21
|
+
# +Middleware::Error#error_response+ can still materialize it there.
|
|
18
22
|
def self.from_exception(exception)
|
|
19
23
|
new(
|
|
20
24
|
status: exception.status,
|
|
21
25
|
message: exception.message,
|
|
22
26
|
headers: exception.headers,
|
|
23
|
-
backtrace: exception.backtrace,
|
|
24
27
|
original_exception: exception
|
|
25
28
|
)
|
|
26
29
|
end
|
|
@@ -9,15 +9,16 @@ module Grape
|
|
|
9
9
|
|
|
10
10
|
def initialize(params:, message: nil, status: nil, headers: nil)
|
|
11
11
|
@params = Array(params)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
translated =
|
|
13
|
+
if message
|
|
14
|
+
@message_key = case message
|
|
15
|
+
when Symbol then message
|
|
16
|
+
when Hash then message[:key]
|
|
17
|
+
end
|
|
18
|
+
translate_message(message)
|
|
19
|
+
end
|
|
19
20
|
|
|
20
|
-
super(status:, message
|
|
21
|
+
super(status:, message: translated, headers:)
|
|
21
22
|
# Pre-seed the backtrace so Ruby's raise skips capture. Validation errors are
|
|
22
23
|
# a hot path (raised per bad attribute) and end up as 400 Bad Request responses;
|
|
23
24
|
# backtraces here point into Grape internals and have no diagnostic value.
|
data/lib/grape/formatter/json.rb
CHANGED
|
@@ -5,6 +5,7 @@ module Grape
|
|
|
5
5
|
class SerializableHash < Base
|
|
6
6
|
class << self
|
|
7
7
|
def call(object, _env)
|
|
8
|
+
return object.to_s if object.is_a?(Grape::PrecompiledJson)
|
|
8
9
|
return object if object.is_a?(String)
|
|
9
10
|
return ::Grape::Json.dump(serialize(object)) if serializable?(object)
|
|
10
11
|
return object.to_json if object.respond_to?(:to_json)
|
data/lib/grape/locale/en.yml
CHANGED
|
@@ -48,6 +48,7 @@ en:
|
|
|
48
48
|
regexp: 'is invalid'
|
|
49
49
|
same_as: 'is not the same as %{parameter}'
|
|
50
50
|
unknown_auth_strategy: 'unknown auth strategy: %{strategy}'
|
|
51
|
+
unknown_error_formatter: 'unknown error formatter: %{error_formatter_type}'
|
|
51
52
|
unknown_options: 'unknown options: %{options}'
|
|
52
53
|
unknown_parameter: 'unknown parameter: %{param}'
|
|
53
54
|
unknown_params_builder: 'unknown params_builder: %{params_builder_type}'
|
|
@@ -4,49 +4,21 @@ module Grape
|
|
|
4
4
|
module Middleware
|
|
5
5
|
module Auth
|
|
6
6
|
module DSL
|
|
7
|
-
def auth(type = nil,
|
|
8
|
-
|
|
9
|
-
return namespace_inheritable[:auth] unless type
|
|
7
|
+
def auth(type = nil, **options, &block)
|
|
8
|
+
return inheritable_setting.auth unless type
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
use Grape::Middleware::Auth::Base, namespace_inheritable[:auth]
|
|
10
|
+
inheritable_setting.auth = { type: type.to_sym, proc: block }.merge!(options)
|
|
11
|
+
use Grape::Middleware::Auth::Base, inheritable_setting.auth
|
|
14
12
|
end
|
|
15
13
|
|
|
16
14
|
# Add HTTP Basic authorization to the API.
|
|
17
15
|
#
|
|
18
16
|
# @param options [Hash] a hash of options
|
|
19
17
|
# @option options [String] :realm "API Authorization" the HTTP Basic realm
|
|
20
|
-
def http_basic(
|
|
21
|
-
options = merge_legacy_auth_options(:http_basic, legacy_options, options)
|
|
18
|
+
def http_basic(**options, &)
|
|
22
19
|
options[:realm] ||= 'API Authorization'
|
|
23
20
|
auth(:http_basic, **options, &)
|
|
24
21
|
end
|
|
25
|
-
|
|
26
|
-
def http_digest(*legacy_options, **options, &)
|
|
27
|
-
options = merge_legacy_auth_options(:http_digest, legacy_options, options)
|
|
28
|
-
options[:realm] ||= 'API Authorization'
|
|
29
|
-
|
|
30
|
-
if options[:realm].respond_to?(:values_at)
|
|
31
|
-
options[:realm][:opaque] ||= 'secret'
|
|
32
|
-
else
|
|
33
|
-
options[:opaque] ||= 'secret'
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
auth(:http_digest, **options, &)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
private
|
|
40
|
-
|
|
41
|
-
# @deprecated Passing a positional options Hash is deprecated; pass
|
|
42
|
-
# keyword arguments instead. Kept so downstream callers keep working
|
|
43
|
-
# through the deprecation cycle.
|
|
44
|
-
def merge_legacy_auth_options(method_name, legacy_options, options)
|
|
45
|
-
return options if legacy_options.empty?
|
|
46
|
-
|
|
47
|
-
Grape.deprecator.warn("Passing a positional options Hash to `#{method_name}` is deprecated. Pass keyword arguments instead.")
|
|
48
|
-
legacy_options.first.merge(options)
|
|
49
|
-
end
|
|
50
22
|
end
|
|
51
23
|
end
|
|
52
24
|
end
|