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
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module DSL
|
|
5
|
+
# Immutable value object holding the response-shaping booleans accepted
|
|
6
|
+
# by +Grape::DSL::RequestResponse#rescue_from+. Stored on the
|
|
7
|
+
# inheritable settings as +namespace_stackable[:rescue_options]+ and
|
|
8
|
+
# delegated to by +Grape::Middleware::Error+ (which forwards
|
|
9
|
+
# +backtrace+/+original_exception+ to the formatter as
|
|
10
|
+
# +include_backtrace+/+include_original_exception+).
|
|
11
|
+
#
|
|
12
|
+
# Defaults are duplicated on +#initialize+ here and on +#rescue_from+'s
|
|
13
|
+
# signature on purpose: keeping them on both sides means each entry point
|
|
14
|
+
# is self-documenting without needing to import a shared constant — the
|
|
15
|
+
# DSL signature shows what a user sees in the IDE, and the Data object
|
|
16
|
+
# has working defaults when constructed directly (middleware
|
|
17
|
+
# `DEFAULT_OPTIONS`, spec fixtures, etc.). The two must stay in lockstep.
|
|
18
|
+
RescueOptions = Data.define(:backtrace, :original_exception) do
|
|
19
|
+
def initialize(backtrace: false, original_exception: false)
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/grape/dsl/routing.rb
CHANGED
|
@@ -23,6 +23,12 @@ module Grape
|
|
|
23
23
|
|
|
24
24
|
# Specify an API version.
|
|
25
25
|
#
|
|
26
|
+
# Called without arguments, returns the most recently declared version
|
|
27
|
+
# (or +nil+). Called with one or more version strings, registers them
|
|
28
|
+
# and stores a {Grape::DSL::VersionOptions} value object on the
|
|
29
|
+
# inheritable settings; when given a block, the registration applies
|
|
30
|
+
# within a nested namespace.
|
|
31
|
+
#
|
|
26
32
|
# @example API with legacy support.
|
|
27
33
|
# class MyAPI < Grape::API
|
|
28
34
|
# version 'v2'
|
|
@@ -38,26 +44,41 @@ module Grape
|
|
|
38
44
|
# end
|
|
39
45
|
# end
|
|
40
46
|
#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
# @param args [Array<String, Symbol>] one or more version identifiers.
|
|
48
|
+
# @param using [Symbol] versioning strategy — one of +:path+ (default),
|
|
49
|
+
# +:header+, +:param+, or +:accept_version_header+.
|
|
50
|
+
# @param cascade [Boolean] forward to subsequent routes via the
|
|
51
|
+
# +X-Cascade+ header on version mismatch. Defaults to +true+.
|
|
52
|
+
# @param parameter [String] name of the query/body parameter that
|
|
53
|
+
# carries the version when +using: :param+. Defaults to +'apiver'+.
|
|
54
|
+
# @param strict [Boolean] reject requests that don't supply a usable
|
|
55
|
+
# version (header strategies). Defaults to +false+.
|
|
56
|
+
# @param vendor [String, nil] vendor segment for the +:header+
|
|
57
|
+
# strategy (+application/vnd.<vendor>-<version>+); required when
|
|
58
|
+
# +using: :header+.
|
|
59
|
+
# @yield optional block to scope routes under this version.
|
|
60
|
+
# @return [String, nil] the most recently declared version.
|
|
61
|
+
# @raise [Grape::Exceptions::MissingVendorOption] when +using: :header+
|
|
62
|
+
# is supplied without a +:vendor+.
|
|
63
|
+
def version(*args, using: :path, cascade: true, parameter: 'apiver', strict: false, vendor: nil, &block)
|
|
64
|
+
return @versions&.last if args.empty?
|
|
65
|
+
|
|
66
|
+
raise Grape::Exceptions::MissingVendorOption.new if using == :header && vendor.nil?
|
|
67
|
+
|
|
68
|
+
requested_versions = args.flatten.map(&:to_s)
|
|
69
|
+
options = VersionOptions.new(using:, cascade:, parameter:, strict:, vendor:)
|
|
70
|
+
|
|
71
|
+
@versions = versions | requested_versions
|
|
72
|
+
|
|
73
|
+
if block
|
|
74
|
+
within_namespace do
|
|
58
75
|
inheritable_setting.namespace_inheritable[:version] = requested_versions
|
|
59
76
|
inheritable_setting.namespace_inheritable[:version_options] = options
|
|
77
|
+
instance_eval(&block)
|
|
60
78
|
end
|
|
79
|
+
else
|
|
80
|
+
inheritable_setting.namespace_inheritable[:version] = requested_versions
|
|
81
|
+
inheritable_setting.namespace_inheritable[:version_options] = options
|
|
61
82
|
end
|
|
62
83
|
|
|
63
84
|
@versions&.last
|
|
@@ -113,7 +134,7 @@ module Grape
|
|
|
113
134
|
in_setting = inheritable_setting
|
|
114
135
|
|
|
115
136
|
if app.respond_to?(:inheritable_setting, true)
|
|
116
|
-
mount_path = Grape::
|
|
137
|
+
mount_path = Grape::Util::PathNormalizer.call(path)
|
|
117
138
|
app.top_level_setting.namespace_stackable[:mount_path] = mount_path
|
|
118
139
|
|
|
119
140
|
app.inherit_settings(inheritable_setting)
|
|
@@ -163,7 +184,7 @@ module Grape
|
|
|
163
184
|
endpoint_description = inheritable_setting.route[:description]
|
|
164
185
|
all_route_options = { params: endpoint_params }
|
|
165
186
|
all_route_options.deep_merge!(endpoint_description) if endpoint_description
|
|
166
|
-
all_route_options.deep_merge!(route_options) if route_options
|
|
187
|
+
all_route_options.deep_merge!(route_options) if route_options.present?
|
|
167
188
|
|
|
168
189
|
new_endpoint = Grape::Endpoint.new(
|
|
169
190
|
inheritable_setting,
|
|
@@ -180,9 +201,8 @@ module Grape
|
|
|
180
201
|
end
|
|
181
202
|
|
|
182
203
|
Grape::HTTP_SUPPORTED_METHODS.each do |supported_method|
|
|
183
|
-
define_method supported_method.downcase do
|
|
184
|
-
|
|
185
|
-
route(supported_method, paths, options, &block)
|
|
204
|
+
define_method supported_method.downcase do |path = '/', **options, &block|
|
|
205
|
+
route(supported_method, path, options, &block)
|
|
186
206
|
end
|
|
187
207
|
end
|
|
188
208
|
|
|
@@ -260,28 +280,24 @@ module Grape
|
|
|
260
280
|
# of settings stack pushes.
|
|
261
281
|
def nest(*blocks, &block)
|
|
262
282
|
blocks.compact!
|
|
263
|
-
if blocks.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
instance_eval(&block)
|
|
269
|
-
end
|
|
283
|
+
return instance_eval(&block) if blocks.empty?
|
|
284
|
+
|
|
285
|
+
evaluate_as_instance_with_configuration(block) if block
|
|
286
|
+
blocks.each { |b| evaluate_as_instance_with_configuration(b) }
|
|
287
|
+
reset_validations!
|
|
270
288
|
end
|
|
271
289
|
|
|
272
290
|
def evaluate_as_instance_with_configuration(block, lazy: false)
|
|
273
291
|
lazy_block = Grape::Util::Lazy::Block.new do |configuration|
|
|
274
292
|
value_for_configuration = configuration
|
|
275
|
-
self.configuration = value_for_configuration.evaluate if value_for_configuration.
|
|
293
|
+
self.configuration = value_for_configuration.evaluate if value_for_configuration.is_a?(Grape::Util::Lazy::Base)
|
|
276
294
|
response = instance_eval(&block)
|
|
277
295
|
self.configuration = value_for_configuration
|
|
278
296
|
response
|
|
279
297
|
end
|
|
280
|
-
if @base && base_instance? && lazy
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
lazy_block.evaluate_from(configuration)
|
|
284
|
-
end
|
|
298
|
+
return lazy_block if @base && base_instance? && lazy
|
|
299
|
+
|
|
300
|
+
lazy_block.evaluate_from(configuration)
|
|
285
301
|
end
|
|
286
302
|
end
|
|
287
303
|
end
|
data/lib/grape/dsl/settings.rb
CHANGED
|
@@ -11,19 +11,25 @@ module Grape
|
|
|
11
11
|
|
|
12
12
|
# Fetch our top-level settings, which apply to all endpoints in the API.
|
|
13
13
|
def top_level_setting
|
|
14
|
-
@top_level_setting
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
return @top_level_setting if @top_level_setting
|
|
15
|
+
|
|
16
|
+
@top_level_setting = Grape::Util::InheritableSetting.new
|
|
17
|
+
# Doesn't try to inherit settings from +Grape::API::Instance+ which also responds to
|
|
18
|
+
# +inheritable_setting+, however, it doesn't contain any user-defined settings.
|
|
19
|
+
# Otherwise, it would lead to an extra instance of +Grape::Util::InheritableSetting+
|
|
20
|
+
# in the chain for every endpoint.
|
|
21
|
+
@top_level_setting.inherit_from superclass.inheritable_setting if defined?(superclass) && superclass.respond_to?(:inheritable_setting) && superclass != Grape::API::Instance
|
|
22
|
+
@top_level_setting
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
# Fetch our current inheritable settings, which are inherited by
|
|
24
26
|
# nested scopes but not shared across siblings.
|
|
25
27
|
def inheritable_setting
|
|
26
|
-
@inheritable_setting
|
|
28
|
+
return @inheritable_setting if @inheritable_setting
|
|
29
|
+
|
|
30
|
+
@inheritable_setting = Grape::Util::InheritableSetting.new
|
|
31
|
+
@inheritable_setting.inherit_from top_level_setting
|
|
32
|
+
@inheritable_setting
|
|
27
33
|
end
|
|
28
34
|
|
|
29
35
|
def global_setting(key, value = nil)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module DSL
|
|
5
|
+
# Immutable value object holding the resolved options from
|
|
6
|
+
# +Grape::DSL::Routing#version+. Stored on the inheritable settings as
|
|
7
|
+
# +namespace_inheritable[:version_options]+ and read by internal call
|
|
8
|
+
# sites (`Path`, `Endpoint`, `API::Instance#cascade?`,
|
|
9
|
+
# `Middleware::Versioner::Base`) via accessors.
|
|
10
|
+
#
|
|
11
|
+
# Defaults are duplicated on +#initialize+ here and on +#version+'s
|
|
12
|
+
# signature on purpose: keeping them on both sides means each entry point
|
|
13
|
+
# is self-documenting without needing to import a shared constant — the
|
|
14
|
+
# DSL signature shows what a user sees in the IDE, and the Data object
|
|
15
|
+
# has working defaults when constructed directly (middleware
|
|
16
|
+
# `DEFAULT_OPTIONS`, spec fixtures, etc.). The two must stay in lockstep.
|
|
17
|
+
VersionOptions = Data.define(:using, :cascade, :parameter, :strict, :vendor) do
|
|
18
|
+
def initialize(using: :path, cascade: true, parameter: 'apiver', strict: false, vendor: nil)
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
class Endpoint
|
|
5
|
+
# Immutable value object holding the keyword inputs passed to
|
|
6
|
+
# +Grape::Endpoint.new+. Internal to {Grape::Endpoint}, which builds it
|
|
7
|
+
# from the +**options+ Hash in #initialize so the public +options+ reader
|
|
8
|
+
# stays a plain Hash for downstream gems (e.g. grape-swagger).
|
|
9
|
+
# +:method+ is renamed to +:http_methods+ on the value object to avoid
|
|
10
|
+
# shadowing +Object#method+ via the generated Data accessor.
|
|
11
|
+
Options = Data.define(:path, :http_methods, :for, :route_options, :app, :format, :forward_match) do
|
|
12
|
+
def initialize(path:, method:, route_options: {}, app: nil, format: nil, forward_match: nil, **rest)
|
|
13
|
+
path = Array(path)
|
|
14
|
+
path << '/' if path.empty?
|
|
15
|
+
super(path:, http_methods: Array(method), route_options:, app:, format:, forward_match:, **rest)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/grape/endpoint.rb
CHANGED
|
@@ -12,29 +12,19 @@ module Grape
|
|
|
12
12
|
include Grape::DSL::InsideRoute
|
|
13
13
|
|
|
14
14
|
attr_reader :env, :request, :source, :options, :endpoints
|
|
15
|
+
attr_accessor :options_route_enabled
|
|
15
16
|
|
|
16
17
|
def_delegators :request, :params, :headers, :cookies
|
|
17
18
|
def_delegator :cookies, :response_cookies
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@before_each << block
|
|
26
|
-
elsif new_setup
|
|
27
|
-
@before_each = [new_setup]
|
|
28
|
-
else
|
|
29
|
-
@before_each.clear
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def run_before_each(endpoint)
|
|
34
|
-
superclass.run_before_each(endpoint) unless self == Endpoint
|
|
35
|
-
before_each.each { |blk| blk.call(endpoint) }
|
|
36
|
-
end
|
|
20
|
+
# The logger configured on the API this endpoint belongs to. Available
|
|
21
|
+
# inside route handlers, +before+/+after+/+after_validation+/+finally+
|
|
22
|
+
# filters, and +rescue_from+ blocks.
|
|
23
|
+
def logger
|
|
24
|
+
config.for.logger
|
|
25
|
+
end
|
|
37
26
|
|
|
27
|
+
class << self
|
|
38
28
|
def block_to_unbound_method(block)
|
|
39
29
|
return unless block
|
|
40
30
|
|
|
@@ -48,7 +38,8 @@ module Grape
|
|
|
48
38
|
# Create a new endpoint.
|
|
49
39
|
# @param new_settings [InheritableSetting] settings to determine the params,
|
|
50
40
|
# validations, and other properties from.
|
|
51
|
-
# @param options [Hash] attributes of this endpoint
|
|
41
|
+
# @param options [Hash] attributes of this endpoint, normalized into a
|
|
42
|
+
# +Grape::Endpoint::Options+ value object.
|
|
52
43
|
# @option options path [String or Array] the path to this endpoint, within
|
|
53
44
|
# the current scope.
|
|
54
45
|
# @option options method [String or Array] which HTTP method(s) can be used
|
|
@@ -64,23 +55,24 @@ module Grape
|
|
|
64
55
|
# this endpoint and its parents, but later it will be cleaned up,
|
|
65
56
|
# see +reset_validations!+ in lib/grape/dsl/validations.rb
|
|
66
57
|
inheritable_setting.route[:declared_params] = inheritable_setting.namespace_stackable[:declared_params].flatten
|
|
67
|
-
inheritable_setting.route[:saved_validations] = inheritable_setting.namespace_stackable[:validations]
|
|
58
|
+
inheritable_setting.route[:saved_validations] = inheritable_setting.namespace_stackable[:validations].dup
|
|
68
59
|
|
|
69
60
|
inheritable_setting.namespace_stackable[:representations] ||= []
|
|
70
61
|
inheritable_setting.namespace_inheritable[:default_error_status] ||= 500
|
|
71
62
|
|
|
72
63
|
@options = options
|
|
73
|
-
|
|
74
64
|
@options[:path] = Array(@options[:path])
|
|
75
65
|
@options[:path] << '/' if @options[:path].empty?
|
|
76
66
|
@options[:method] = Array(@options[:method])
|
|
67
|
+
@config = Options.new(**options)
|
|
77
68
|
|
|
78
69
|
@status = nil
|
|
79
70
|
@stream = nil
|
|
80
71
|
@body = nil
|
|
81
72
|
@source = self.class.block_to_unbound_method(block)
|
|
82
73
|
@before_filter_passed = false
|
|
83
|
-
@
|
|
74
|
+
@options_route_enabled = false
|
|
75
|
+
@endpoints = @config.app.endpoints if @config.app.respond_to?(:endpoints)
|
|
84
76
|
end
|
|
85
77
|
|
|
86
78
|
# Update our settings from a given set of stackable parameters. Used when
|
|
@@ -141,8 +133,8 @@ module Grape
|
|
|
141
133
|
|
|
142
134
|
def ==(other)
|
|
143
135
|
other.is_a?(self.class) &&
|
|
144
|
-
|
|
145
|
-
inheritable_setting
|
|
136
|
+
config == other.config &&
|
|
137
|
+
inheritable_setting == other.inheritable_setting
|
|
146
138
|
end
|
|
147
139
|
alias eql? ==
|
|
148
140
|
|
|
@@ -158,10 +150,9 @@ module Grape
|
|
|
158
150
|
protected
|
|
159
151
|
|
|
160
152
|
def run
|
|
161
|
-
|
|
162
|
-
@request = Grape::Request.new(env, build_params_with:
|
|
153
|
+
instrument_run do
|
|
154
|
+
@request = Grape::Request.new(env, build_params_with: @build_params_with)
|
|
163
155
|
begin
|
|
164
|
-
self.class.run_before_each(self)
|
|
165
156
|
run_filters befores, :before
|
|
166
157
|
@before_filter_passed = true
|
|
167
158
|
|
|
@@ -169,7 +160,6 @@ module Grape
|
|
|
169
160
|
header['Allow'] = env[Grape::Env::GRAPE_ALLOWED_METHODS].join(', ')
|
|
170
161
|
raise Grape::Exceptions::MethodNotAllowed.new(header) unless options?
|
|
171
162
|
|
|
172
|
-
header 'Allow', header['Allow']
|
|
173
163
|
response_object = ''
|
|
174
164
|
status 204
|
|
175
165
|
else
|
|
@@ -198,77 +188,104 @@ module Grape
|
|
|
198
188
|
def execute
|
|
199
189
|
return unless source
|
|
200
190
|
|
|
201
|
-
|
|
191
|
+
instrument_render do
|
|
202
192
|
source.bind_call(self)
|
|
203
193
|
end
|
|
204
194
|
end
|
|
205
195
|
|
|
206
196
|
def run_validators(request:)
|
|
207
197
|
validators = inheritable_setting.route[:saved_validations]
|
|
208
|
-
return if validators.
|
|
198
|
+
return if validators.blank?
|
|
209
199
|
|
|
210
|
-
|
|
200
|
+
validation_exceptions = nil
|
|
211
201
|
|
|
212
202
|
Grape::Validations::ParamScopeTracker.track do
|
|
213
|
-
|
|
203
|
+
instrument_run_validators(validators, request) do
|
|
214
204
|
validators.each do |validator|
|
|
215
205
|
validator.validate(request)
|
|
216
|
-
rescue Grape::Exceptions::Validation => e
|
|
217
|
-
|
|
218
|
-
break if validator.fail_fast?
|
|
219
|
-
rescue Grape::Exceptions::ValidationArrayErrors => e
|
|
220
|
-
validation_errors.concat e.errors
|
|
206
|
+
rescue Grape::Exceptions::Validation, Grape::Exceptions::ValidationArrayErrors => e
|
|
207
|
+
(validation_exceptions ||= []) << e
|
|
221
208
|
break if validator.fail_fast?
|
|
222
209
|
end
|
|
223
210
|
end
|
|
224
211
|
end
|
|
225
212
|
|
|
226
|
-
raise
|
|
213
|
+
raise Grape::Exceptions::ValidationErrors.new(exceptions: validation_exceptions, headers: header) if validation_exceptions
|
|
227
214
|
end
|
|
228
215
|
|
|
229
216
|
def run_filters(filters, type = :other)
|
|
230
|
-
return
|
|
217
|
+
return if filters.blank?
|
|
231
218
|
|
|
232
|
-
|
|
219
|
+
instrument_run_filters(filters, type) do
|
|
233
220
|
filters.each { |filter| instance_eval(&filter) }
|
|
234
221
|
end
|
|
235
222
|
end
|
|
236
223
|
|
|
237
|
-
|
|
238
|
-
define_method method do
|
|
239
|
-
inheritable_setting.namespace_stackable[method]
|
|
240
|
-
end
|
|
241
|
-
end
|
|
224
|
+
attr_reader :befores, :before_validations, :after_validations, :afters, :finallies, :config
|
|
242
225
|
|
|
243
226
|
def options?
|
|
244
|
-
|
|
245
|
-
env[Rack::REQUEST_METHOD] == Rack::OPTIONS
|
|
227
|
+
options_route_enabled && env[Rack::REQUEST_METHOD] == Rack::OPTIONS
|
|
246
228
|
end
|
|
247
229
|
|
|
248
230
|
private
|
|
249
231
|
|
|
250
232
|
attr_reader :before_filter_passed
|
|
251
233
|
|
|
234
|
+
# Instrument helpers. Each guards on +listening?+ so that with no subscriber
|
|
235
|
+
# the payload Hash and notification machinery are skipped and the block runs
|
|
236
|
+
# directly (no added allocations); the block is forwarded anonymously so
|
|
237
|
+
# nothing is allocated unless a subscriber is present.
|
|
238
|
+
def instrument_run(&)
|
|
239
|
+
return yield unless ActiveSupport::Notifications.notifier.listening?('endpoint_run.grape')
|
|
240
|
+
|
|
241
|
+
ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env:, &)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def instrument_render(&)
|
|
245
|
+
return yield unless ActiveSupport::Notifications.notifier.listening?('endpoint_render.grape')
|
|
246
|
+
|
|
247
|
+
ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: self, &)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def instrument_run_validators(validators, request, &)
|
|
251
|
+
return yield unless ActiveSupport::Notifications.notifier.listening?('endpoint_run_validators.grape')
|
|
252
|
+
|
|
253
|
+
ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators:, request:, &)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def instrument_run_filters(filters, type, &)
|
|
257
|
+
return yield unless ActiveSupport::Notifications.notifier.listening?('endpoint_run_filters.grape')
|
|
258
|
+
|
|
259
|
+
ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters:, type:, &)
|
|
260
|
+
end
|
|
261
|
+
|
|
252
262
|
def compile!
|
|
253
|
-
@app =
|
|
263
|
+
@app = config.app || build_stack
|
|
254
264
|
@helpers = build_helpers
|
|
265
|
+
stackable = inheritable_setting.namespace_stackable
|
|
266
|
+
@befores = stackable[:befores]
|
|
267
|
+
@before_validations = stackable[:before_validations]
|
|
268
|
+
@after_validations = stackable[:after_validations]
|
|
269
|
+
@afters = stackable[:afters]
|
|
270
|
+
@finallies = stackable[:finallies]
|
|
271
|
+
@build_params_with = inheritable_setting.namespace_inheritable[:build_params_with]
|
|
255
272
|
end
|
|
256
273
|
|
|
257
274
|
def to_routes
|
|
258
|
-
route_options =
|
|
275
|
+
route_options = config.route_options
|
|
259
276
|
default_route_options = prepare_default_route_attributes(route_options)
|
|
260
277
|
complete_route_options = route_options.merge(default_route_options)
|
|
261
278
|
path_settings = prepare_default_path_settings
|
|
262
279
|
|
|
263
|
-
|
|
264
|
-
|
|
280
|
+
config.http_methods.flat_map do |method|
|
|
281
|
+
config.path.map do |path|
|
|
265
282
|
prepared_path = Path.new(path, default_route_options[:namespace], path_settings)
|
|
266
283
|
pattern = Grape::Router::Pattern.new(
|
|
267
284
|
origin: prepared_path.origin,
|
|
268
285
|
suffix: prepared_path.suffix,
|
|
269
286
|
anchor: default_route_options[:anchor],
|
|
270
287
|
params: route_options[:params],
|
|
271
|
-
format:
|
|
288
|
+
format: config.format,
|
|
272
289
|
version: default_route_options[:version],
|
|
273
290
|
requirements: default_route_options[:requirements]
|
|
274
291
|
)
|
|
@@ -285,7 +302,7 @@ module Grape
|
|
|
285
302
|
prefix: inheritable_setting.namespace_inheritable[:root_prefix],
|
|
286
303
|
anchor: route_options.fetch(:anchor, true),
|
|
287
304
|
settings: inheritable_setting.route.except(:declared_params, :saved_validations),
|
|
288
|
-
forward_match:
|
|
305
|
+
forward_match: config.forward_match
|
|
289
306
|
}
|
|
290
307
|
end
|
|
291
308
|
|
|
@@ -315,26 +332,15 @@ module Grape
|
|
|
315
332
|
|
|
316
333
|
stack.use Rack::Head
|
|
317
334
|
stack.use Rack::Lint if lint?
|
|
318
|
-
stack.use Grape::Middleware::Error,
|
|
319
|
-
format:,
|
|
320
|
-
content_types:,
|
|
321
|
-
default_status: inheritable_setting.namespace_inheritable[:default_error_status],
|
|
322
|
-
rescue_all: inheritable_setting.namespace_inheritable[:rescue_all],
|
|
323
|
-
rescue_grape_exceptions: inheritable_setting.namespace_inheritable[:rescue_grape_exceptions],
|
|
324
|
-
default_error_formatter: inheritable_setting.namespace_inheritable[:default_error_formatter],
|
|
325
|
-
error_formatters: inheritable_setting.namespace_stackable_with_hash(:error_formatters),
|
|
326
|
-
rescue_options: inheritable_setting.namespace_stackable_with_hash(:rescue_options),
|
|
327
|
-
rescue_handlers:,
|
|
328
|
-
base_only_rescue_handlers: inheritable_setting.namespace_stackable_with_hash(:base_only_rescue_handlers),
|
|
329
|
-
all_rescue_handler: inheritable_setting.namespace_inheritable[:all_rescue_handler],
|
|
330
|
-
grape_exceptions_rescue_handler: inheritable_setting.namespace_inheritable[:grape_exceptions_rescue_handler]
|
|
335
|
+
stack.use Grape::Middleware::Error, **error_middleware_options(format, content_types)
|
|
331
336
|
|
|
332
337
|
stack.concat inheritable_setting.namespace_stackable[:middleware]
|
|
333
338
|
|
|
334
339
|
if inheritable_setting.namespace_inheritable[:version].present?
|
|
335
|
-
|
|
340
|
+
version_options = inheritable_setting.namespace_inheritable[:version_options]
|
|
341
|
+
stack.use Grape::Middleware::Versioner.using(version_options.using),
|
|
336
342
|
versions: inheritable_setting.namespace_inheritable[:version].flatten,
|
|
337
|
-
version_options
|
|
343
|
+
version_options:,
|
|
338
344
|
prefix: inheritable_setting.namespace_inheritable[:root_prefix],
|
|
339
345
|
mount_path: inheritable_setting.namespace_stackable[:mount_path].first
|
|
340
346
|
end
|
|
@@ -351,6 +357,26 @@ module Grape
|
|
|
351
357
|
builder.to_app
|
|
352
358
|
end
|
|
353
359
|
|
|
360
|
+
def error_middleware_options(format, content_types)
|
|
361
|
+
ns_inh = inheritable_setting.namespace_inheritable
|
|
362
|
+
ns_stack = inheritable_setting
|
|
363
|
+
{
|
|
364
|
+
format:,
|
|
365
|
+
content_types:,
|
|
366
|
+
default_status: ns_inh[:default_error_status],
|
|
367
|
+
rescue_all: ns_inh[:rescue_all],
|
|
368
|
+
rescue_grape_exceptions: ns_inh[:rescue_grape_exceptions],
|
|
369
|
+
default_error_formatter: ns_inh[:default_error_formatter],
|
|
370
|
+
error_formatters: ns_stack.namespace_stackable_with_hash(:error_formatters),
|
|
371
|
+
rescue_options: ns_stack.namespace_stackable[:rescue_options]&.last,
|
|
372
|
+
rescue_handlers:,
|
|
373
|
+
base_only_rescue_handlers: ns_stack.namespace_stackable_with_hash(:base_only_rescue_handlers),
|
|
374
|
+
all_rescue_handler: ns_inh[:all_rescue_handler],
|
|
375
|
+
grape_exceptions_rescue_handler: ns_inh[:grape_exceptions_rescue_handler],
|
|
376
|
+
internal_grape_exceptions_rescue_handler: ns_inh[:internal_grape_exceptions_rescue_handler]
|
|
377
|
+
}
|
|
378
|
+
end
|
|
379
|
+
|
|
354
380
|
def build_helpers
|
|
355
381
|
helpers = inheritable_setting.namespace_stackable[:helpers]
|
|
356
382
|
return if helpers.empty?
|
|
@@ -359,6 +385,8 @@ module Grape
|
|
|
359
385
|
end
|
|
360
386
|
|
|
361
387
|
def build_response_cookies
|
|
388
|
+
return unless request.cookies?
|
|
389
|
+
|
|
362
390
|
response_cookies do |name, value|
|
|
363
391
|
cookie_value = value.is_a?(Hash) ? value : { value: }
|
|
364
392
|
Rack::Utils.set_cookie_header! header, name, cookie_value
|
data/lib/grape/env.rb
CHANGED
|
@@ -11,10 +11,8 @@ module Grape
|
|
|
11
11
|
API_VENDOR = 'api.vendor'
|
|
12
12
|
API_FORMAT = 'api.format'
|
|
13
13
|
|
|
14
|
-
GRAPE_REQUEST = 'grape.request'
|
|
15
|
-
GRAPE_REQUEST_HEADERS = 'grape.request.headers'
|
|
16
|
-
GRAPE_REQUEST_PARAMS = 'grape.request.params'
|
|
17
14
|
GRAPE_ROUTING_ARGS = 'grape.routing_args'
|
|
18
15
|
GRAPE_ALLOWED_METHODS = 'grape.allowed_methods'
|
|
16
|
+
GRAPE_EXCEPTION = 'grape.exception'
|
|
19
17
|
end
|
|
20
18
|
end
|
|
@@ -4,34 +4,39 @@ module Grape
|
|
|
4
4
|
module ErrorFormatter
|
|
5
5
|
class Base
|
|
6
6
|
class << self
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
# Custom error formatters override +call+. The +error+ is a frozen
|
|
8
|
+
# {Grape::Exceptions::ErrorResponse} carrying +status+/+message+/
|
|
9
|
+
# +headers+/+backtrace+/+original_exception+. +env+ is the Rack env
|
|
10
|
+
# (needed by entity-presenter resolution). +include_backtrace+ and
|
|
11
|
+
# +include_original_exception+ are the request-time toggles set by
|
|
12
|
+
# +rescue_from+; the base implementation embeds the corresponding
|
|
13
|
+
# fields in the response body when they are true.
|
|
14
|
+
def call(error:, env: nil, include_backtrace: false, include_original_exception: false)
|
|
15
|
+
wrapped_message = wrap_message(present(error.message, env))
|
|
12
16
|
if wrapped_message.is_a?(Hash)
|
|
13
|
-
wrapped_message[:backtrace] = backtrace if
|
|
14
|
-
wrapped_message[:original_exception] = original_exception.inspect if
|
|
17
|
+
wrapped_message[:backtrace] = error.backtrace if include_backtrace && error.backtrace.present?
|
|
18
|
+
wrapped_message[:original_exception] = error.original_exception.inspect if include_original_exception && error.original_exception
|
|
15
19
|
end
|
|
16
20
|
|
|
17
21
|
format_structured_message(wrapped_message)
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
def present(message, env)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
# error! accepts a message hash with an optional :with key specifying the entity presenter.
|
|
26
|
+
# Extract it here so the presenter can be resolved and the key is not serialized in the response.
|
|
27
|
+
# See spec/integration/grape_entity/entity_spec.rb for examples.
|
|
28
|
+
with = nil
|
|
29
|
+
if message.is_a?(Hash) && message.key?(:with)
|
|
30
|
+
message = message.dup
|
|
31
|
+
with = message.delete(:with)
|
|
26
32
|
end
|
|
27
33
|
|
|
28
|
-
presenter = env[Grape::Env::API_ENDPOINT].entity_class_for_obj(
|
|
34
|
+
presenter = with || env[Grape::Env::API_ENDPOINT].entity_class_for_obj(message)
|
|
29
35
|
|
|
30
36
|
unless presenter || env[Grape::Env::GRAPE_ROUTING_ARGS].nil?
|
|
31
37
|
# env['api.endpoint'].route does not work when the error occurs within a middleware
|
|
32
38
|
# the Endpoint does not have a valid env at this moment
|
|
33
39
|
http_codes = env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info].http_codes || []
|
|
34
|
-
|
|
35
40
|
found_code = http_codes.find do |http_code|
|
|
36
41
|
(http_code[0].to_i == env[Grape::Env::API_ENDPOINT].status) && http_code[2].respond_to?(:represent)
|
|
37
42
|
end if env[Grape::Env::API_ENDPOINT].request
|
|
@@ -39,13 +44,11 @@ module Grape
|
|
|
39
44
|
presenter = found_code[2] if found_code
|
|
40
45
|
end
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
embeds = { env: }
|
|
44
|
-
embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
|
|
45
|
-
presented_message = presenter.represent(presented_message, embeds).serializable_hash
|
|
46
|
-
end
|
|
47
|
+
return message unless presenter
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
embeds = { env: }
|
|
50
|
+
embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
|
|
51
|
+
presenter.represent(message, embeds).serializable_hash
|
|
49
52
|
end
|
|
50
53
|
|
|
51
54
|
def wrap_message(message)
|
|
@@ -11,10 +11,14 @@ module Grape
|
|
|
11
11
|
private
|
|
12
12
|
|
|
13
13
|
def wrap_message(message)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
case message
|
|
15
|
+
when Hash
|
|
16
|
+
message
|
|
17
|
+
when Exceptions::ValidationErrors
|
|
18
|
+
message.as_json
|
|
19
|
+
else
|
|
20
|
+
{ error: ensure_utf8(message) }
|
|
21
|
+
end
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
def ensure_utf8(message)
|