grape 4.0.0 → 4.1.0

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