grape 3.2.1 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -0
  3. data/README.md +116 -43
  4. data/UPGRADING.md +336 -1
  5. data/grape.gemspec +5 -5
  6. data/lib/grape/api/instance.rb +7 -7
  7. data/lib/grape/api.rb +22 -25
  8. data/lib/grape/cookies.rb +2 -6
  9. data/lib/grape/declared_params_handler.rb +48 -50
  10. data/lib/grape/dsl/callbacks.rb +9 -3
  11. data/lib/grape/dsl/desc.rb +8 -2
  12. data/lib/grape/dsl/entity.rb +88 -0
  13. data/lib/grape/dsl/helpers.rb +27 -7
  14. data/lib/grape/dsl/inside_route.rb +38 -129
  15. data/lib/grape/dsl/logger.rb +3 -5
  16. data/lib/grape/dsl/parameters.rb +32 -38
  17. data/lib/grape/dsl/request_response.rb +53 -48
  18. data/lib/grape/dsl/rescue_options.rb +24 -0
  19. data/lib/grape/dsl/routing.rb +51 -35
  20. data/lib/grape/dsl/settings.rb +14 -8
  21. data/lib/grape/dsl/version_options.rb +23 -0
  22. data/lib/grape/endpoint/options.rb +19 -0
  23. data/lib/grape/endpoint.rb +96 -68
  24. data/lib/grape/env.rb +1 -3
  25. data/lib/grape/error_formatter/base.rb +23 -20
  26. data/lib/grape/error_formatter/json.rb +8 -4
  27. data/lib/grape/error_formatter/txt.rb +10 -10
  28. data/lib/grape/exceptions/base.rb +3 -1
  29. data/lib/grape/exceptions/error_response.rb +45 -0
  30. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  31. data/lib/grape/exceptions/validation.rb +14 -0
  32. data/lib/grape/exceptions/validation_array_errors.rb +4 -0
  33. data/lib/grape/exceptions/validation_errors.rb +12 -20
  34. data/lib/grape/formatter/serializable_hash.rb +5 -9
  35. data/lib/grape/json.rb +38 -2
  36. data/lib/grape/locale/en.yml +2 -0
  37. data/lib/grape/middleware/auth/base.rb +2 -3
  38. data/lib/grape/middleware/auth/dsl.rb +23 -8
  39. data/lib/grape/middleware/base.rb +22 -33
  40. data/lib/grape/middleware/deprecated_options_hash_access.rb +19 -0
  41. data/lib/grape/middleware/error.rb +152 -62
  42. data/lib/grape/middleware/formatter.rb +66 -50
  43. data/lib/grape/middleware/precomputed_content_types.rb +46 -0
  44. data/lib/grape/middleware/stack.rb +5 -6
  45. data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
  46. data/lib/grape/middleware/versioner/base.rb +34 -38
  47. data/lib/grape/middleware/versioner/header.rb +3 -5
  48. data/lib/grape/middleware/versioner/path.rb +8 -3
  49. data/lib/grape/namespace.rb +3 -3
  50. data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
  51. data/lib/grape/parser/json.rb +1 -1
  52. data/lib/grape/path.rb +14 -17
  53. data/lib/grape/request.rb +15 -8
  54. data/lib/grape/router/mustermann_pattern.rb +44 -0
  55. data/lib/grape/router/pattern.rb +6 -10
  56. data/lib/grape/router.rb +28 -42
  57. data/lib/grape/serve_stream/file_body.rb +1 -0
  58. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  59. data/lib/grape/serve_stream/stream_response.rb +1 -0
  60. data/lib/grape/testing.rb +33 -0
  61. data/lib/grape/util/base_inheritable.rb +13 -16
  62. data/lib/grape/util/inheritable_setting.rb +44 -27
  63. data/lib/grape/util/inheritable_values.rb +7 -3
  64. data/lib/grape/util/lazy/base.rb +16 -0
  65. data/lib/grape/util/lazy/block.rb +2 -9
  66. data/lib/grape/util/lazy/value.rb +2 -9
  67. data/lib/grape/util/lazy/value_enumerable.rb +13 -16
  68. data/lib/grape/util/media_type.rb +1 -4
  69. data/lib/grape/util/path_normalizer.rb +34 -0
  70. data/lib/grape/util/registry.rb +1 -1
  71. data/lib/grape/util/stackable_values.rb +11 -8
  72. data/lib/grape/validations/attributes_iterator.rb +13 -13
  73. data/lib/grape/validations/coerce_options.rb +21 -0
  74. data/lib/grape/validations/oneof_collector.rb +39 -0
  75. data/lib/grape/validations/param_scope_tracker.rb +14 -9
  76. data/lib/grape/validations/params_documentation.rb +25 -23
  77. data/lib/grape/validations/params_scope.rb +54 -172
  78. data/lib/grape/validations/shared_options.rb +19 -0
  79. data/lib/grape/validations/types/array_coercer.rb +2 -2
  80. data/lib/grape/validations/types/custom_type_coercer.rb +41 -85
  81. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  82. data/lib/grape/validations/types/dry_type_coercer.rb +3 -3
  83. data/lib/grape/validations/types/primitive_coercer.rb +10 -5
  84. data/lib/grape/validations/types/set_coercer.rb +1 -1
  85. data/lib/grape/validations/types/variant_collection_coercer.rb +8 -0
  86. data/lib/grape/validations/types.rb +23 -30
  87. data/lib/grape/validations/validations_spec.rb +149 -0
  88. data/lib/grape/validations/validators/all_or_none_of_validator.rb +1 -1
  89. data/lib/grape/validations/validators/at_least_one_of_validator.rb +1 -1
  90. data/lib/grape/validations/validators/base.rb +39 -22
  91. data/lib/grape/validations/validators/coerce_validator.rb +5 -3
  92. data/lib/grape/validations/validators/default_validator.rb +7 -8
  93. data/lib/grape/validations/validators/except_values_validator.rb +3 -2
  94. data/lib/grape/validations/validators/length_validator.rb +1 -1
  95. data/lib/grape/validations/validators/multiple_params_base.rb +10 -7
  96. data/lib/grape/validations/validators/oneof_validator.rb +49 -0
  97. data/lib/grape/validations/validators/values_validator.rb +5 -5
  98. data/lib/grape/version.rb +1 -1
  99. data/lib/grape/xml.rb +8 -1
  100. data/lib/grape.rb +6 -6
  101. metadata +34 -18
  102. data/lib/grape/middleware/globals.rb +0 -14
@@ -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
@@ -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
- def version(*args, **options, &block)
42
- if args.any?
43
- options = options.reverse_merge(using: :path)
44
- requested_versions = args.flatten.map(&:to_s)
45
-
46
- raise Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.key?(:vendor)
47
-
48
- @versions = versions | requested_versions
49
-
50
- if block
51
- within_namespace do
52
- inheritable_setting.namespace_inheritable[:version] = requested_versions
53
- inheritable_setting.namespace_inheritable[:version_options] = options
54
-
55
- instance_eval(&block)
56
- end
57
- else
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::Router.normalize_path(path)
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&.any?
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 |*args, **options, &block|
184
- paths = args.first || ['/']
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.any?
264
- evaluate_as_instance_with_configuration(block) if block
265
- blocks.each { |b| evaluate_as_instance_with_configuration(b) }
266
- reset_validations!
267
- else
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.respond_to?(:lazy?) && value_for_configuration.lazy?
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
- lazy_block
282
- else
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
@@ -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 ||= Grape::Util::InheritableSetting.new.tap do |setting|
15
- # Doesn't try to inherit settings from +Grape::API::Instance+ which also responds to
16
- # +inheritable_setting+, however, it doesn't contain any user-defined settings.
17
- # Otherwise, it would lead to an extra instance of +Grape::Util::InheritableSetting+
18
- # in the chain for every endpoint.
19
- setting.inherit_from superclass.inheritable_setting if defined?(superclass) && superclass.respond_to?(:inheritable_setting) && superclass != Grape::API::Instance
20
- end
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 ||= Grape::Util::InheritableSetting.new.tap { |new_settings| new_settings.inherit_from top_level_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
@@ -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
- class << self
20
- def before_each(new_setup = false, &block)
21
- @before_each ||= []
22
- if new_setup == false
23
- return @before_each unless block
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
- @endpoints = @options[:app].endpoints if @options[:app].respond_to?(:endpoints)
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
- options == other.options &&
145
- inheritable_setting.to_hash == other.inheritable_setting.to_hash
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
- ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env:) do
162
- @request = Grape::Request.new(env, build_params_with: inheritable_setting.namespace_inheritable[: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
- ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: self) do
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.empty?
198
+ return if validators.blank?
209
199
 
210
- validation_errors = []
200
+ validation_exceptions = nil
211
201
 
212
202
  Grape::Validations::ParamScopeTracker.track do
213
- ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators:, request:) do
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
- validation_errors << e
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(Grape::Exceptions::ValidationErrors.new(errors: validation_errors, headers: header)) if validation_errors.any?
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 unless filters
217
+ return if filters.blank?
231
218
 
232
- ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters:, type:) do
219
+ instrument_run_filters(filters, type) do
233
220
  filters.each { |filter| instance_eval(&filter) }
234
221
  end
235
222
  end
236
223
 
237
- %i[befores before_validations after_validations afters finallies].each do |method|
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
- options[:options_route_enabled] &&
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 = options[:app] || build_stack
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 = options[: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
- options[:method].flat_map do |method|
264
- options[:path].map do |path|
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: options[: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: options[: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
- stack.use Grape::Middleware::Versioner.using(inheritable_setting.namespace_inheritable[:version_options][:using]),
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: inheritable_setting.namespace_inheritable[: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
- def call(message, backtrace, options = {}, env = nil, original_exception = nil)
8
- merge_backtrace = backtrace.present? && options.dig(:rescue_options, :backtrace)
9
- merge_original_exception = original_exception && options.dig(:rescue_options, :original_exception)
10
-
11
- wrapped_message = wrap_message(present(message, env))
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 merge_backtrace
14
- wrapped_message[:original_exception] = original_exception.inspect if merge_original_exception
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
- present_options = {}
22
- presented_message = message
23
- if presented_message.is_a?(Hash)
24
- presented_message = presented_message.dup
25
- present_options[:with] = presented_message.delete(:with)
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(presented_message, present_options)
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
- if presenter
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
- presented_message
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
- return message if message.is_a?(Hash)
15
- return message.as_json if message.is_a?(Exceptions::ValidationErrors)
16
-
17
- { error: ensure_utf8(message) }
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)