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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -0
  3. data/CONTRIBUTING.md +16 -0
  4. data/README.md +208 -8
  5. data/UPGRADING.md +756 -6
  6. data/lib/grape/api/instance.rb +40 -33
  7. data/lib/grape/content_types.rb +47 -1
  8. data/lib/grape/dry_types.rb +1 -1
  9. data/lib/grape/dsl/callbacks.rb +3 -9
  10. data/lib/grape/dsl/declared.rb +4 -4
  11. data/lib/grape/dsl/desc.rb +18 -8
  12. data/lib/grape/dsl/entity.rb +37 -19
  13. data/lib/grape/dsl/helpers.rb +3 -3
  14. data/lib/grape/dsl/inside_route.rb +20 -8
  15. data/lib/grape/dsl/middleware.rb +3 -3
  16. data/lib/grape/dsl/parameters.rb +54 -28
  17. data/lib/grape/dsl/request_response.rb +37 -39
  18. data/lib/grape/dsl/rescue_options.rb +6 -5
  19. data/lib/grape/dsl/routing.rb +116 -49
  20. data/lib/grape/dsl/settings.rb +1 -1
  21. data/lib/grape/dsl/validations.rb +3 -3
  22. data/lib/grape/dsl/version_options.rb +3 -3
  23. data/lib/grape/endpoint/options.rb +13 -7
  24. data/lib/grape/endpoint.rb +141 -118
  25. data/lib/grape/env.rb +7 -0
  26. data/lib/grape/error_formatter/base.rb +6 -5
  27. data/lib/grape/error_formatter.rb +6 -2
  28. data/lib/grape/exceptions/error_response.rb +4 -1
  29. data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
  30. data/lib/grape/exceptions/validation.rb +9 -8
  31. data/lib/grape/formatter/json.rb +1 -0
  32. data/lib/grape/formatter/serializable_hash.rb +1 -0
  33. data/lib/grape/locale/en.yml +1 -0
  34. data/lib/grape/middleware/auth/dsl.rb +5 -33
  35. data/lib/grape/middleware/error.rb +153 -46
  36. data/lib/grape/middleware/formatter.rb +95 -26
  37. data/lib/grape/middleware/precomputed_content_types.rb +12 -7
  38. data/lib/grape/middleware/stack.rb +13 -4
  39. data/lib/grape/middleware/versioner/base.rb +3 -9
  40. data/lib/grape/middleware/versioner/path.rb +49 -7
  41. data/lib/grape/namespace.rb +3 -2
  42. data/lib/grape/path.rb +8 -69
  43. data/lib/grape/precompiled_json.rb +50 -0
  44. data/lib/grape/request.rb +36 -4
  45. data/lib/grape/router/base_route.rb +47 -10
  46. data/lib/grape/router/greedy_route.rb +5 -1
  47. data/lib/grape/router/pattern/path.rb +78 -0
  48. data/lib/grape/router/pattern.rb +60 -14
  49. data/lib/grape/router/route.rb +62 -10
  50. data/lib/grape/router.rb +100 -57
  51. data/lib/grape/serve_stream/file_body.rb +6 -0
  52. data/lib/grape/serve_stream/stream_response.rb +6 -0
  53. data/lib/grape/util/api_description.rb +10 -1
  54. data/lib/grape/util/cache.rb +21 -2
  55. data/lib/grape/util/deep_freeze.rb +1 -2
  56. data/lib/grape/util/freeze_on_new.rb +20 -0
  57. data/lib/grape/util/inheritable_setting.rb +751 -37
  58. data/lib/grape/util/media_type.rb +10 -3
  59. data/lib/grape/util/path_normalizer.rb +12 -9
  60. data/lib/grape/util/registry.rb +12 -2
  61. data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
  62. data/lib/grape/util/stackable_values.rb +32 -6
  63. data/lib/grape/validations/attributes_iterator.rb +21 -4
  64. data/lib/grape/validations/contract_scope.rb +8 -7
  65. data/lib/grape/validations/oneof_collector.rb +6 -10
  66. data/lib/grape/validations/params_documentation.rb +2 -2
  67. data/lib/grape/validations/params_scope.rb +66 -26
  68. data/lib/grape/validations/single_attribute_iterator.rb +5 -3
  69. data/lib/grape/validations/types/array_coercer.rb +4 -6
  70. data/lib/grape/validations/types/custom_type_coercer.rb +7 -1
  71. data/lib/grape/validations/types/dry_type_coercer.rb +3 -1
  72. data/lib/grape/validations/types/json.rb +1 -3
  73. data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
  74. data/lib/grape/validations/types/primitive_coercer.rb +12 -9
  75. data/lib/grape/validations/types/variant_collection_coercer.rb +8 -3
  76. data/lib/grape/validations/types.rb +5 -5
  77. data/lib/grape/validations/validations_spec.rb +14 -4
  78. data/lib/grape/validations/validators/base.rb +4 -7
  79. data/lib/grape/validations/validators/coerce_validator.rb +8 -2
  80. data/lib/grape/validations/validators/contract_scope_validator.rb +2 -1
  81. data/lib/grape/validations/validators/length_validator.rb +4 -2
  82. data/lib/grape/validations/validators/oneof_validator.rb +2 -0
  83. data/lib/grape/validations/validators/same_as_validator.rb +1 -0
  84. data/lib/grape/version.rb +2 -1
  85. data/lib/grape.rb +23 -2
  86. metadata +10 -9
  87. data/lib/grape/middleware/deprecated_options_hash_access.rb +0 -19
  88. data/lib/grape/util/base_inheritable.rb +0 -43
  89. data/lib/grape/util/inheritable_values.rb +0 -33
  90. data/lib/grape/util/reverse_stackable_values.rb +0 -15
@@ -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
- def logger
24
- config.for.logger
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.point_in_time_copy
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
- @options[:path] = Array(@options[:path])
65
- @options[:path] << '/' if @options[:path].empty?
66
- @options[:method] = Array(@options[:method])
67
- @config = Options.new(**options)
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 set of stackable parameters. Used when
87
+ # Update our settings from a given parent settings instance. Used when
79
88
  # the endpoint's API is mounted under another one.
80
- def inherit_settings(namespace_stackable)
81
- parent_validations = namespace_stackable[:validations]
82
- inheritable_setting.route[:saved_validations].concat(parent_validations) if parent_validations.any?
83
- parent_declared_params = namespace_stackable[:declared_params]
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.namespace_inheritable[:do_not_route_head] || route.request_method != Rack::GET
115
+ next if inheritable_setting.do_not_route_head? || route.request_method != Rack::GET
110
116
 
111
- route.dup.then do |head_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 ||= Namespace.joined_space_path(inheritable_setting.namespace_stackable[: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.route[:saved_validations]
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
- stackable = inheritable_setting.namespace_stackable
267
- @befores = stackable[:befores]
268
- @before_validations = stackable[:before_validations]
269
- @after_validations = stackable[:after_validations]
270
- @afters = stackable[:afters]
271
- @finallies = stackable[:finallies]
272
- @build_params_with = inheritable_setting.namespace_inheritable[:build_params_with]
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
- default_route_options = prepare_default_route_attributes(route_options)
278
- complete_route_options = route_options.merge(default_route_options)
279
- path_settings = prepare_default_path_settings
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
- prepared_path = Path.new(path, default_route_options[:namespace], path_settings)
284
- pattern = Grape::Router::Pattern.new(
285
- origin: prepared_path.origin,
286
- suffix: prepared_path.suffix,
287
- anchor: default_route_options[:anchor],
288
- params: route_options[:params],
289
- format: config.format,
290
- version: default_route_options[:version],
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, complete_route_options)
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.namespace_stackable[:namespace].filter_map(&:requirements)
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.namespace_stackable_with_hash(:content_types)
339
- format = inheritable_setting.namespace_inheritable[:format]
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.namespace_stackable[:middleware]
382
+ stack.concat inheritable_setting.middleware
346
383
 
347
- if inheritable_setting.namespace_inheritable[:version].present?
348
- version_options = inheritable_setting.namespace_inheritable[:version_options]
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.namespace_inheritable[:version].flatten,
387
+ versions: inheritable_setting.version.flatten,
351
388
  version_options:,
352
- prefix: inheritable_setting.namespace_inheritable[:root_prefix],
353
- mount_path: inheritable_setting.namespace_stackable[:mount_path].first
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.namespace_inheritable[:default_format] || :txt,
395
+ default_format: inheritable_setting.default_format || :txt,
359
396
  content_types:,
360
- formatters: inheritable_setting.namespace_stackable_with_hash(:formatters),
361
- parsers: inheritable_setting.namespace_stackable_with_hash(:parsers)
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
- ns_inh = inheritable_setting.namespace_inheritable
370
- ns_stack = inheritable_setting
406
+ setting = inheritable_setting
371
407
  {
372
408
  format:,
373
409
  content_types:,
374
- default_status: ns_inh[:default_error_status],
375
- rescue_all: ns_inh[:rescue_all],
376
- rescue_grape_exceptions: ns_inh[:rescue_grape_exceptions],
377
- default_error_formatter: ns_inh[:default_error_formatter],
378
- error_formatters: ns_stack.namespace_stackable_with_hash(:error_formatters),
379
- rescue_options: ns_stack.namespace_stackable[:rescue_options]&.last,
380
- rescue_handlers: merged_reverse_stackable(:rescue_handlers),
381
- base_only_rescue_handlers: merged_reverse_stackable(:base_only_rescue_handlers),
382
- all_rescue_handler: ns_inh[:all_rescue_handler],
383
- grape_exceptions_rescue_handler: ns_inh[:grape_exceptions_rescue_handler],
384
- internal_grape_exceptions_rescue_handler: ns_inh[: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.namespace_stackable[:helpers]
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.namespace_inheritable[:auth]
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.namespace_inheritable[:lint] || Grape.config.lint
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
- message = message.dup
31
- with = message.delete(: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(message)
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 message unless presenter
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(message, embeds).serializable_hash
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
- def formatter_for(format, error_formatters = nil, default_error_formatter = nil)
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] || default_error_formatter || Grape::ErrorFormatter::Txt
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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Exceptions
5
+ class UnknownErrorFormatter < Base
6
+ def initialize(error_formatter_type)
7
+ super(message: compose_message(:unknown_error_formatter, error_formatter_type:))
8
+ end
9
+ end
10
+ end
11
+ 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
- if message
13
- @message_key = case message
14
- when Symbol then message
15
- when Hash then message[:key]
16
- end
17
- message = translate_message(message)
18
- end
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:, headers:)
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.
@@ -4,6 +4,7 @@ module Grape
4
4
  module Formatter
5
5
  class Json < Base
6
6
  def self.call(object, _env)
7
+ return object.to_s if object.is_a?(Grape::PrecompiledJson)
7
8
  return object.to_json if object.respond_to?(:to_json)
8
9
 
9
10
  ::Grape::Json.dump(object)
@@ -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)
@@ -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, *legacy_options, **options, &block)
8
- namespace_inheritable = inheritable_setting.namespace_inheritable
9
- return namespace_inheritable[:auth] unless type
7
+ def auth(type = nil, **options, &block)
8
+ return inheritable_setting.auth unless type
10
9
 
11
- options = merge_legacy_auth_options(:auth, legacy_options, options)
12
- namespace_inheritable[:auth] = { type: type.to_sym, proc: block }.merge!(options)
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(*legacy_options, **options, &)
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