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
data/UPGRADING.md
CHANGED
|
@@ -1,6 +1,341 @@
|
|
|
1
1
|
Upgrading Grape
|
|
2
2
|
===============
|
|
3
3
|
|
|
4
|
+
### Upgrading to >= 3.3
|
|
5
|
+
|
|
6
|
+
#### Minimum required Ruby is now 3.3
|
|
7
|
+
|
|
8
|
+
Grape no longer supports Ruby 3.2; 3.3 is now the minimum (`required_ruby_version = '>= 3.3'`). Upgrade your runtime to Ruby 3.3 or newer before bumping Grape.
|
|
9
|
+
|
|
10
|
+
#### `mustermann-grape` is no longer a dependency
|
|
11
|
+
|
|
12
|
+
Grape's path-pattern grammar (previously the `mustermann-grape` gem) now lives in Grape itself as `Grape::Router::MustermannPattern`, and Grape depends on `mustermann` directly. This is transparent for normal Grape usage.
|
|
13
|
+
|
|
14
|
+
The inlined class is no longer registered as a Mustermann type, so if your app called `Mustermann.new(pattern, type: :grape)` and relied on Grape loading `mustermann-grape` for you, add it to your Gemfile explicitly:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem 'mustermann-grape'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
#### `Grape::Exceptions::ValidationErrors.new` keyword renamed `errors:` → `exceptions:`
|
|
21
|
+
|
|
22
|
+
`Grape::Exceptions::ValidationErrors#initialize` now takes its input array under the `exceptions:` keyword instead of `errors:`. The kwarg accepts a mix of `Grape::Exceptions::Validation` and `Grape::Exceptions::ValidationArrayErrors` instances; `ValidationArrayErrors` wrappers are flattened internally via `flat_map(&:errors)`. The `errors` reader on the constructed instance (the grouped `{params => [Validation, ...]}` Hash) is unchanged.
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
# before
|
|
26
|
+
Grape::Exceptions::ValidationErrors.new(errors: [validation, validation_array_errors], headers:)
|
|
27
|
+
|
|
28
|
+
# after
|
|
29
|
+
Grape::Exceptions::ValidationErrors.new(exceptions: [validation, validation_array_errors], headers:)
|
|
30
|
+
#### `Grape::Exceptions::ValidationErrors` no longer mixes in `Enumerable`
|
|
31
|
+
|
|
32
|
+
`Grape::Exceptions::ValidationErrors` no longer includes `Enumerable` and no longer defines a public `#each`. The Enumerable surface (`#each`, `#map`, `#select`, `#to_a`, etc.) was undocumented and untested; the documented accessors — `#errors`, `#full_messages`, `#message`, `#as_json` — are unchanged.
|
|
33
|
+
|
|
34
|
+
If a `rescue_from` block iterated over the exception instance, switch to `#errors`:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# before
|
|
38
|
+
rescue_from Grape::Exceptions::ValidationErrors do |e|
|
|
39
|
+
e.each { |attribute, error| ... }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# after
|
|
43
|
+
rescue_from Grape::Exceptions::ValidationErrors do |e|
|
|
44
|
+
e.errors.each do |attributes, errs|
|
|
45
|
+
errs.each { |error| ... }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### `rescue_from` rejects meta selectors mixed with exception classes
|
|
51
|
+
|
|
52
|
+
`rescue_from` used to silently drop additional exception classes when its first argument was a meta selector (`:all`, `:grape_exceptions`, `:internal_grape_exceptions`). It now raises `ArgumentError` so the misuse is caught at definition time:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
# previously: MyError was silently dropped — only :all took effect
|
|
56
|
+
rescue_from :all, MyError, with: :handler
|
|
57
|
+
|
|
58
|
+
# now: ArgumentError ("rescue_from :all does not accept additional arguments")
|
|
59
|
+
# split into two declarations instead:
|
|
60
|
+
rescue_from :all, with: :handler
|
|
61
|
+
rescue_from MyError, with: :other_handler
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Calls that only use one meta selector or only use exception classes (the documented forms) are unaffected.
|
|
65
|
+
|
|
66
|
+
#### `auth`, `http_basic` and `http_digest` now take keyword arguments
|
|
67
|
+
|
|
68
|
+
`Grape::Middleware::Auth::DSL#auth`, `#http_basic` and `#http_digest` now accept their options as keyword arguments instead of a positional `Hash`. Calls using bare keyword syntax or a block are unaffected:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
http_basic realm: 'API' do |u, p|
|
|
72
|
+
# ...
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
auth :http_digest, realm: 'API', opaque: 'secret', &proc
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Passing a positional options `Hash` still works but is deprecated and will be removed in a future release:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
# deprecated
|
|
82
|
+
http_basic({ realm: 'API' })
|
|
83
|
+
auth :http_digest, { realm: 'API', opaque: 'secret' }
|
|
84
|
+
|
|
85
|
+
# preferred
|
|
86
|
+
http_basic(realm: 'API')
|
|
87
|
+
auth :http_digest, realm: 'API', opaque: 'secret'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### Middleware options now route through per-class `Options` `Data` value objects
|
|
91
|
+
|
|
92
|
+
`Grape::Middleware::Error`, `Grape::Middleware::Formatter`, and `Grape::Middleware::Versioner::Base` each declare an `Options` `Data.define` and route their `**options` kwargs through it on `initialize`. This means **unknown kwargs now raise `ArgumentError`** instead of being silently swallowed:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
# previously: silently swallowed (Formatter doesn't actually read :rescue_options)
|
|
96
|
+
Grape::Middleware::Formatter.new(app, rescue_options: { backtrace: true })
|
|
97
|
+
|
|
98
|
+
# now: ArgumentError (unknown keyword: :rescue_options)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Each `Options` class accepts exactly the kwargs the middleware actually reads. The supported sets:
|
|
102
|
+
|
|
103
|
+
- `Middleware::Error::Options`: `all_rescue_handler`, `base_only_rescue_handlers`, `content_types`, `default_error_formatter`, `default_message`, `default_status`, `error_formatters`, `format`, `grape_exceptions_rescue_handler`, `internal_grape_exceptions_rescue_handler`, `rescue_all`, `rescue_grape_exceptions`, `rescue_handlers`, `rescue_options`.
|
|
104
|
+
- `Middleware::Formatter::Options`: `content_types`, `default_format`, `format`, `formatters`, `parsers`.
|
|
105
|
+
- `Middleware::Versioner::Base::Options`: `content_types`, `format`, `mount_path`, `pattern`, `prefix`, `version_options`, `versions`.
|
|
106
|
+
|
|
107
|
+
The `Hash`-based `options` reader on `Grape::Middleware::Base` continues to return a frozen Hash representation of the Data (`config.to_h.freeze`) for back-compat with subclasses that read `options[:key]`. A new `config` reader exposes the typed Data instance — prefer the named accessors going forward:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# back-compat (still works)
|
|
111
|
+
options[:format]
|
|
112
|
+
|
|
113
|
+
# preferred
|
|
114
|
+
config.format
|
|
115
|
+
# or, on converted middlewares, just `format` (provided via def_delegators)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`Options#[]` is defined as a Hash-style shim with a deprecation warning so legacy `data[:key]` callers get a migration nudge:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
# emits Grape.deprecator warning
|
|
122
|
+
Grape::Middleware::Error::Options.new[:format]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### `DEFAULT_OPTIONS` constants on converted middlewares are deprecated
|
|
126
|
+
|
|
127
|
+
`Grape::Middleware::Error::DEFAULT_OPTIONS`, `Grape::Middleware::Formatter::DEFAULT_OPTIONS`, and `Grape::Middleware::Versioner::Base::DEFAULT_OPTIONS` still exist as a frozen `Hash` representation of the `Options` defaults (`Options.new.to_h.freeze`), for back-compat with any code that referenced these constants directly. They will be removed in a future release; introspect the `Options` `Data` class itself instead.
|
|
128
|
+
|
|
129
|
+
#### `Grape::Middleware::Globals` removed
|
|
130
|
+
|
|
131
|
+
`Grape::Middleware::Globals` and the three env constants it set (`Grape::Env::GRAPE_REQUEST`, `Grape::Env::GRAPE_REQUEST_HEADERS`, `Grape::Env::GRAPE_REQUEST_PARAMS`) have been deleted. The middleware was introduced in 2013 (commit `9987090b`) but never mounted by Grape's own stack — the `Grape::Request` it built is now constructed directly inside `Grape::Endpoint`. Nothing in `lib/` read those env keys.
|
|
132
|
+
|
|
133
|
+
If you mounted `Grape::Middleware::Globals` in your own Rack stack to populate `env['grape.request']` for downstream middleware, replicate it locally:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
class MyGlobals
|
|
137
|
+
def initialize(app); @app = app; end
|
|
138
|
+
|
|
139
|
+
def call(env)
|
|
140
|
+
request = Grape::Request.new(env)
|
|
141
|
+
env['grape.request'] = request
|
|
142
|
+
env['grape.request.headers'] = request.headers
|
|
143
|
+
env['grape.request.params'] = request.params if env['rack.input']
|
|
144
|
+
@app.call(env)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The original implementation is preserved in git history at [`6b4111b3:lib/grape/middleware/globals.rb`](https://github.com/ruby-grape/grape/blob/6b4111b3/lib/grape/middleware/globals.rb).
|
|
150
|
+
|
|
151
|
+
#### `error_formatter` now receives a `Grape::Exceptions::ErrorResponse` value object
|
|
152
|
+
|
|
153
|
+
Custom error formatters now receive a frozen `Grape::Exceptions::ErrorResponse` as the `error:` keyword argument, alongside three request-time context kwargs. The new signature:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
def call(error:, env: nil, include_backtrace: false, include_original_exception: false)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`error` is the same value object the middleware uses internally, with `status` / `message` / `headers` / `backtrace` / `original_exception` accessors. The two `include_*` booleans are forwarded from the matching `rescue_from` options (previously buried inside `options[:rescue_options]`).
|
|
160
|
+
|
|
161
|
+
Existing positional formatters break and need to be updated:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
# Before
|
|
165
|
+
error_formatter :txt, ->(message, backtrace, options, env, original_exception) { ... }
|
|
166
|
+
|
|
167
|
+
module CustomFormatter
|
|
168
|
+
def self.call(message, backtrace, options, env, original_exception)
|
|
169
|
+
...
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# After — pick fields off `error`
|
|
174
|
+
error_formatter :txt, ->(error:, **) { "[#{error.status}] #{error.message}" }
|
|
175
|
+
|
|
176
|
+
module CustomFormatter
|
|
177
|
+
def self.call(error:, **)
|
|
178
|
+
{ status: error.status, message: error.message, backtrace: error.backtrace }
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Migration:
|
|
184
|
+
|
|
185
|
+
| Old positional arg | New |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| `message` | `error.message` |
|
|
188
|
+
| `backtrace` | `error.backtrace` |
|
|
189
|
+
| `original_exception` | `error.original_exception` |
|
|
190
|
+
| `options[:rescue_options][:backtrace]` | `include_backtrace` (kwarg) |
|
|
191
|
+
| `options[:rescue_options][:original_exception]` | `include_original_exception` (kwarg) |
|
|
192
|
+
| `env` | `env` (kwarg, still passed) |
|
|
193
|
+
| HTTP status | `error.status` (newly exposed) |
|
|
194
|
+
| Response headers | `error.headers` (newly exposed) |
|
|
195
|
+
|
|
196
|
+
The remaining middleware-options keys (`default_status`, `format`, `rescue_handlers`, …) were framework-internal and have never been part of the documented contract.
|
|
197
|
+
|
|
198
|
+
The change resolves [#2527](https://github.com/ruby-grape/grape/issues/2527): the HTTP `status` and the response `headers` are now part of the formatter contract, so JSON:API–style error bodies (which embed the status code) and header-aware formatters can be written without reaching into `env[Grape::Env::API_ENDPOINT]`.
|
|
199
|
+
|
|
200
|
+
#### `version` now takes explicit keyword arguments
|
|
201
|
+
|
|
202
|
+
`version` previously accepted `**options` and silently ignored any keys it didn't use. It now declares its options explicitly:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
def version(*args, using: :path, cascade: true, parameter: 'apiver', strict: false, vendor: nil, &block)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Passing an unrecognised keyword now raises `ArgumentError` instead of being swallowed. The most common offender is `format:` — it was never a `version` option (response format is set with `format`/`default_format`, and header-versioned requests carry the format in their `Accept` header), but the old splat let `version 'v1', using: :header, vendor: 'x', format: :json` through as a no-op.
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
# Before — `format:` silently ignored
|
|
212
|
+
version 'v1', using: :header, vendor: 'x', format: :json
|
|
213
|
+
|
|
214
|
+
# After
|
|
215
|
+
version 'v1', using: :header, vendor: 'x' # set responses with `format :json` / `default_format :json`
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Recognized keys are `using:`, `cascade:`, `parameter:`, `strict:`, `vendor:`. Calls using only those are unaffected.
|
|
219
|
+
|
|
220
|
+
#### `Grape::Middleware::Base#options` is now frozen
|
|
221
|
+
|
|
222
|
+
`@options` is frozen at the end of `Grape::Middleware::Base#initialize` (after `merge_default_options`). The hash is initialized once and treated as immutable for the lifetime of the middleware. Custom middleware that mutates `options[...]` at runtime will now raise `FrozenError`.
|
|
223
|
+
|
|
224
|
+
If your custom middleware was patching its own options on the fly:
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
# Before
|
|
228
|
+
class MyMiddleware < Grape::Middleware::Base
|
|
229
|
+
def before
|
|
230
|
+
options[:flag] = compute_flag
|
|
231
|
+
# ...
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# After — store mutable runtime state on a dedicated ivar
|
|
236
|
+
class MyMiddleware < Grape::Middleware::Base
|
|
237
|
+
def before
|
|
238
|
+
@flag = compute_flag
|
|
239
|
+
# ...
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Reading `options[...]` is unchanged.
|
|
245
|
+
|
|
246
|
+
#### Throw `:error` payloads are now `Grape::Exceptions::ErrorResponse`
|
|
247
|
+
|
|
248
|
+
The payload thrown via `throw :error, ...` is now a `Grape::Exceptions::ErrorResponse` value object instead of a `Hash`. If you `catch(:error)` and inspect the payload, switch from `payload[:status]` to `payload.status` (or `payload[:message]` to `payload.message`, etc.). User-defined `throw :error, hash` calls continue to work — `Middleware::Error#error_response` coerces Hashes, exceptions, and `ErrorResponse` instances at the boundary.
|
|
249
|
+
|
|
250
|
+
Returning or throwing a `Hash` with `:message`, `:status`, and `:headers` from a `rescue_from` handler is now deprecated and will be removed in a future release. Use `error!(...)` or return/throw a `Grape::Exceptions::ErrorResponse` instead.
|
|
251
|
+
|
|
252
|
+
#### `Grape::Request#grape_routing_args` has been removed
|
|
253
|
+
|
|
254
|
+
`grape_routing_args` was previously public to support third-party `params_builder` extensions, which have since been removed. With no remaining callers, the method has been removed. If you were calling it externally, read `env[Grape::Env::GRAPE_ROUTING_ARGS]` directly.
|
|
255
|
+
|
|
256
|
+
#### `endpoint_run_filters.grape` notification no longer fired for empty filter lists
|
|
257
|
+
|
|
258
|
+
`ActiveSupport::Notifications` subscribers listening to `endpoint_run_filters.grape` will no longer receive an event when the filter list for a given phase (`:before`, `:before_validation`, `:after_validation`, `:after`, `:finally`) is empty. Previously every phase emitted an event on every request regardless of whether any filters were registered. If you relied on these events to infer per-phase timing, subscribe to `endpoint_run.grape` (which always fires once per request) or register a no-op filter to keep the phase instrumented.
|
|
259
|
+
|
|
260
|
+
#### `Grape::Endpoint.before_each` moved to `Grape::Testing`
|
|
261
|
+
|
|
262
|
+
`Grape::Endpoint.before_each` and `Grape::Endpoint.reset_before_each` are now only available after requiring `grape/testing`. This module is intended for test environments only and is not loaded by default.
|
|
263
|
+
|
|
264
|
+
Add the following to your test helper:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
require 'grape/testing'
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The `before_each` method now always requires a block — calling it without one raises `ArgumentError`. To clear registered hooks, use the new dedicated `reset_before_each` method:
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
# Before
|
|
274
|
+
after { Grape::Endpoint.before_each nil }
|
|
275
|
+
|
|
276
|
+
# After
|
|
277
|
+
after { Grape::Endpoint.reset_before_each }
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### `Grape::Endpoint#logger` now returns the API's configured logger
|
|
281
|
+
|
|
282
|
+
Calling `logger` inside a route handler, filter (`before` / `before_validation` / `after_validation` / `after` / `finally`), or `rescue_from` block previously raised `NoMethodError` unless the application defined a helper:
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
class MyAPI < Grape::API
|
|
286
|
+
logger Logger.new($stdout)
|
|
287
|
+
|
|
288
|
+
helpers do
|
|
289
|
+
def logger
|
|
290
|
+
MyAPI.logger
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
`Grape::Endpoint` now exposes `#logger` directly, so the helper is no longer necessary:
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
class MyAPI < Grape::API
|
|
300
|
+
logger Logger.new($stdout)
|
|
301
|
+
# logger is now reachable inside route handlers, filters, and rescue_from blocks
|
|
302
|
+
end
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**Helper override still wins.** Helpers are mixed into the endpoint's singleton class via `singleton_class.include(@helpers)`, and singleton-class methods take precedence over instance methods on `Grape::Endpoint`. If your application already defines `logger` in a `helpers` block (or a module included via `helpers`), that definition continues to override `Endpoint#logger`. You can safely keep the helper or remove it — both paths produce the same result for the canonical `MyAPI.logger` case above.
|
|
306
|
+
|
|
307
|
+
**Behaviour change for code that didn't define a helper.** If your code references `logger` inside an endpoint context *without* a corresponding `helpers` definition, that call previously raised `NoMethodError` and now returns the API's configured logger. This is almost always the intended behaviour, but if you were relying on the `NoMethodError` (for instance to short-circuit logging in test environments via `rescue NoMethodError`), update your code to check `respond_to?(:logger)` or to gate logging on a feature flag.
|
|
308
|
+
|
|
309
|
+
#### Exceptions raised inside `rescue_from` blocks are now caught
|
|
310
|
+
|
|
311
|
+
Previously, an exception raised inside a `rescue_from` block was uncaught and bubbled up to Rack, producing the Rack default 500 page. The framework now catches and routes it:
|
|
312
|
+
|
|
313
|
+
1. If the re-raised exception's class has a registered `rescue_from` handler, that handler runs (one redispatch only — a second raise stops the chain).
|
|
314
|
+
2. If the re-raised exception is a `Grape::Exceptions::Base` subclass, it is rendered via the default Grape error path with its own `status` and `message`.
|
|
315
|
+
3. Otherwise, the original exception is exposed on `env['grape.exception']` for upstream Rack middleware to observe, and the response is a generic `Grape::Exceptions::InternalServerError` (`500 Internal Server Error`) — the original exception's message is **not** rendered to the API consumer.
|
|
316
|
+
|
|
317
|
+
This means deliberate re-raises in a `rescue_from` block (e.g. translating one exception class into another) now compose with the rest of your `rescue_from` configuration, and accidental crashes (typos, `NoMethodError`, …) no longer leak internal detail to API consumers.
|
|
318
|
+
|
|
319
|
+
The framework deliberately does **not** log unhandled internal exceptions itself — formatting and destination are application concerns. To log, forward to an error tracker, or customize the response shape for these errors, register a `rescue_from :internal_grape_exceptions` handler:
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
rescue_from :internal_grape_exceptions do |e|
|
|
323
|
+
Sentry.capture_exception(e)
|
|
324
|
+
error!({ message: 'Something went wrong' }, 500)
|
|
325
|
+
end
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
When this handler is registered, the framework hands the original exception to you and you own the response shape entirely.
|
|
329
|
+
|
|
330
|
+
If you relied on the old behaviour and want raw exception messages exposed in development, register a catch-all handler:
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
rescue_from StandardError do |e|
|
|
334
|
+
error!({ message: e.message, class: e.class.name }, 500)
|
|
335
|
+
end
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
|
|
4
339
|
### Upgrading to >= 3.2
|
|
5
340
|
|
|
6
341
|
#### Rack parameter parsing errors now raise `Grape::Exceptions::RequestError`
|
|
@@ -694,7 +1029,7 @@ class Api < Grape::API
|
|
|
694
1029
|
params[:my_param]
|
|
695
1030
|
end
|
|
696
1031
|
get '/example', params: { my_param: nil }
|
|
697
|
-
# 1.3.
|
|
1032
|
+
# 1.3.3 = []
|
|
698
1033
|
# 1.3.2 = nil
|
|
699
1034
|
end
|
|
700
1035
|
```
|
data/grape.gemspec
CHANGED
|
@@ -21,13 +21,13 @@ Gem::Specification.new do |s|
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
s.add_dependency 'activesupport', '>= 7.2'
|
|
24
|
-
s.add_dependency 'dry-configurable'
|
|
24
|
+
s.add_dependency 'dry-configurable', '>= 1.0'
|
|
25
25
|
s.add_dependency 'dry-types', '>= 1.1'
|
|
26
|
-
s.add_dependency 'mustermann
|
|
27
|
-
s.add_dependency 'rack', '>= 2'
|
|
28
|
-
s.add_dependency 'zeitwerk'
|
|
26
|
+
s.add_dependency 'mustermann', '>= 4.0'
|
|
27
|
+
s.add_dependency 'rack', '>= 2.2.4'
|
|
28
|
+
s.add_dependency 'zeitwerk', '>= 2.6'
|
|
29
29
|
|
|
30
30
|
s.files = Dir['lib/**/*', 'CHANGELOG.md', 'CONTRIBUTING.md', 'README.md', 'grape.png', 'UPGRADING.md', 'LICENSE', 'grape.gemspec']
|
|
31
31
|
s.require_paths = ['lib']
|
|
32
|
-
s.required_ruby_version = '>= 3.
|
|
32
|
+
s.required_ruby_version = '>= 3.3'
|
|
33
33
|
end
|
data/lib/grape/api/instance.rb
CHANGED
|
@@ -132,7 +132,7 @@ module Grape
|
|
|
132
132
|
def cascade?
|
|
133
133
|
namespace_inheritable = self.class.inheritable_setting.namespace_inheritable
|
|
134
134
|
return namespace_inheritable[:cascade] if namespace_inheritable.key?(:cascade)
|
|
135
|
-
return namespace_inheritable[:version_options]
|
|
135
|
+
return namespace_inheritable[:version_options].cascade if namespace_inheritable[:version_options]
|
|
136
136
|
|
|
137
137
|
true
|
|
138
138
|
end
|
|
@@ -168,25 +168,25 @@ module Grape
|
|
|
168
168
|
allowed_methods |= [Rack::HEAD] if !namespace_inheritable[:do_not_route_head] && allowed_methods.include?(Rack::GET)
|
|
169
169
|
|
|
170
170
|
allow_header = namespace_inheritable[:do_not_route_options] ? allowed_methods : [Rack::OPTIONS] | allowed_methods
|
|
171
|
-
last_route.app.
|
|
171
|
+
last_route.app.options_route_enabled = true unless namespace_inheritable[:do_not_route_options] || allowed_methods.include?(Rack::OPTIONS)
|
|
172
172
|
|
|
173
173
|
greedy_route = Grape::Router::GreedyRoute.new(last_route.pattern, endpoint: last_route.app, allow_header:)
|
|
174
174
|
@router.associate_routes(greedy_route)
|
|
175
175
|
end
|
|
176
176
|
end
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
private_constant :
|
|
178
|
+
ROOT_PREFIX_VERSIONING_KEYS = %i[version version_options root_prefix].freeze
|
|
179
|
+
private_constant :ROOT_PREFIX_VERSIONING_KEYS
|
|
180
180
|
|
|
181
181
|
# Allows definition of endpoints that ignore the versioning configuration
|
|
182
182
|
# used by the rest of your API.
|
|
183
183
|
def without_root_prefix_and_versioning
|
|
184
184
|
inheritable_setting = self.class.inheritable_setting
|
|
185
|
-
deleted_values = inheritable_setting.namespace_inheritable.delete(*
|
|
185
|
+
deleted_values = inheritable_setting.namespace_inheritable.delete(*ROOT_PREFIX_VERSIONING_KEYS)
|
|
186
186
|
yield
|
|
187
187
|
ensure
|
|
188
|
-
|
|
189
|
-
inheritable_setting.namespace_inheritable[key] =
|
|
188
|
+
ROOT_PREFIX_VERSIONING_KEYS.zip(deleted_values) do |key, value|
|
|
189
|
+
inheritable_setting.namespace_inheritable[key] = value
|
|
190
190
|
end
|
|
191
191
|
end
|
|
192
192
|
end
|
data/lib/grape/api.rb
CHANGED
|
@@ -10,8 +10,11 @@ module Grape
|
|
|
10
10
|
Helpers = Grape::DSL::Helpers::BaseHelper
|
|
11
11
|
|
|
12
12
|
class Boolean
|
|
13
|
+
VALUES = [true, false].freeze
|
|
14
|
+
private_constant :VALUES
|
|
15
|
+
|
|
13
16
|
def self.build(val)
|
|
14
|
-
return
|
|
17
|
+
return unless VALUES.include?(val)
|
|
15
18
|
|
|
16
19
|
new
|
|
17
20
|
end
|
|
@@ -56,12 +59,10 @@ module Grape
|
|
|
56
59
|
# `configuration` as normal.
|
|
57
60
|
def configure
|
|
58
61
|
config = @base_instance.configuration
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
config
|
|
64
|
-
end
|
|
62
|
+
return config unless block_given?
|
|
63
|
+
|
|
64
|
+
yield config
|
|
65
|
+
self
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
# The remountable class can have a configuration hash to provide some dynamic class-level variables.
|
|
@@ -69,11 +70,11 @@ module Grape
|
|
|
69
70
|
# depending on where the endpoint is mounted. Use with care, if you find yourself using configuration
|
|
70
71
|
# too much, you may actually want to provide a new API rather than remount it.
|
|
71
72
|
def mount_instance(configuration: nil)
|
|
72
|
-
Class.new(@base_parent)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
instance = Class.new(@base_parent)
|
|
74
|
+
instance.configuration = Grape::Util::EndpointConfiguration.new(configuration || {})
|
|
75
|
+
instance.base = self
|
|
76
|
+
replay_setup_on(instance)
|
|
77
|
+
instance
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
private
|
|
@@ -126,11 +127,10 @@ module Grape
|
|
|
126
127
|
eval_args = evaluate_arguments(instance.configuration, *args)
|
|
127
128
|
eval_kwargs = kwargs.deep_transform_values { |v| evaluate_arguments(instance.configuration, v).first }
|
|
128
129
|
response = instance.__send__(method, *eval_args, **eval_kwargs, &block)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
end
|
|
130
|
+
|
|
131
|
+
return response if skip_immediate_run?(instance, [response], kwargs)
|
|
132
|
+
|
|
133
|
+
evaluate_arguments(instance.configuration, response).first
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
# Skips steps that contain arguments to be lazily executed (on re-mount time)
|
|
@@ -140,26 +140,23 @@ module Grape
|
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
def any_lazy?(args)
|
|
143
|
-
args.any?
|
|
143
|
+
args.any?(Grape::Util::Lazy::Base)
|
|
144
144
|
end
|
|
145
145
|
|
|
146
146
|
def evaluate_arguments(configuration, *args)
|
|
147
147
|
args.map do |argument|
|
|
148
|
-
|
|
148
|
+
case argument
|
|
149
|
+
when Grape::Util::Lazy::Base
|
|
149
150
|
argument.evaluate_from(configuration)
|
|
150
|
-
|
|
151
|
+
when Hash
|
|
151
152
|
argument.transform_values { |value| evaluate_arguments(configuration, value).first }
|
|
152
|
-
|
|
153
|
+
when Array
|
|
153
154
|
evaluate_arguments(configuration, *argument)
|
|
154
155
|
else
|
|
155
156
|
argument
|
|
156
157
|
end
|
|
157
158
|
end
|
|
158
159
|
end
|
|
159
|
-
|
|
160
|
-
def argument_lazy?(argument)
|
|
161
|
-
argument.respond_to?(:lazy?) && argument.lazy?
|
|
162
|
-
end
|
|
163
160
|
end
|
|
164
161
|
end
|
|
165
162
|
end
|
data/lib/grape/cookies.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Grape
|
|
|
13
13
|
def_delegators :cookies, :[], :each
|
|
14
14
|
|
|
15
15
|
def initialize(rack_cookies)
|
|
16
|
-
@cookies = rack_cookies
|
|
16
|
+
@cookies = ActiveSupport::HashWithIndifferentAccess.new(rack_cookies)
|
|
17
17
|
@send_cookies = nil
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -37,11 +37,7 @@ module Grape
|
|
|
37
37
|
|
|
38
38
|
private
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
return @cookies unless @cookies.is_a?(Proc)
|
|
42
|
-
|
|
43
|
-
@cookies = @cookies.call.with_indifferent_access
|
|
44
|
-
end
|
|
40
|
+
attr_reader :cookies
|
|
45
41
|
|
|
46
42
|
def send_cookies
|
|
47
43
|
@send_cookies ||= Set.new
|
|
@@ -50,69 +50,67 @@ module Grape
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
def declared_hash_attr(passed_params, declared_param:, params_nested_path:, memo:, renamed_params:, route_params:)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
memo_key = build_memo_key(params_nested_path, declared_parent_param, renamed_params)
|
|
58
|
-
passed_children_params = passed_params[declared_parent_param] || passed_params.class.new
|
|
59
|
-
|
|
60
|
-
params_nested_path_dup = params_nested_path.dup
|
|
61
|
-
params_nested_path_dup << declared_parent_param.to_s
|
|
62
|
-
|
|
63
|
-
memo[memo_key] = handle_passed_param(params_nested_path_dup, route_params:, has_passed_children: passed_children_params.any?) do
|
|
64
|
-
recursive_declared(
|
|
65
|
-
passed_children_params,
|
|
66
|
-
declared_params: declared_children_params,
|
|
67
|
-
params_nested_path: params_nested_path_dup,
|
|
68
|
-
renamed_params:,
|
|
69
|
-
route_params:
|
|
70
|
-
)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
else
|
|
74
|
-
# If it is not a Hash then it does not have children.
|
|
75
|
-
# Find its value or set it to nil.
|
|
76
|
-
return unless @include_missing || (passed_params.respond_to?(:key?) && passed_params.key?(declared_param))
|
|
77
|
-
|
|
78
|
-
memo_key = build_memo_key(params_nested_path, declared_param, renamed_params)
|
|
79
|
-
passed_param = passed_params[declared_param]
|
|
80
|
-
|
|
81
|
-
params_nested_path_dup = params_nested_path.dup
|
|
82
|
-
params_nested_path_dup << declared_param.to_s
|
|
83
|
-
|
|
84
|
-
memo[memo_key] = passed_param || handle_passed_param(params_nested_path_dup, route_params:) do
|
|
85
|
-
passed_param
|
|
86
|
-
end
|
|
53
|
+
return declare_leaf(passed_params, declared_param:, params_nested_path:, memo:, renamed_params:, route_params:) unless declared_param.is_a?(Hash)
|
|
54
|
+
|
|
55
|
+
declared_param.each_pair do |parent, children|
|
|
56
|
+
declare_nested(passed_params, parent:, children:, params_nested_path:, memo:, renamed_params:, route_params:)
|
|
87
57
|
end
|
|
88
58
|
end
|
|
89
59
|
|
|
90
|
-
def
|
|
91
|
-
|
|
92
|
-
|
|
60
|
+
def declare_nested(passed_params, parent:, children:, params_nested_path:, memo:, renamed_params:, route_params:)
|
|
61
|
+
return unless @include_missing || passed_params.key?(parent)
|
|
62
|
+
|
|
63
|
+
memo_key = build_memo_key(params_nested_path, parent, renamed_params)
|
|
64
|
+
passed_children = passed_params[parent] || passed_params.class.new
|
|
65
|
+
nested_path = nested_path_for(params_nested_path, parent)
|
|
66
|
+
|
|
67
|
+
memo[memo_key] = handle_passed_param(nested_path, route_params:, has_passed_children: passed_children.any?) do
|
|
68
|
+
recursive_declared(
|
|
69
|
+
passed_children,
|
|
70
|
+
declared_params: children,
|
|
71
|
+
params_nested_path: nested_path,
|
|
72
|
+
renamed_params:,
|
|
73
|
+
route_params:
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The declared param has no children. Find its value or set it to nil.
|
|
79
|
+
def declare_leaf(passed_params, declared_param:, params_nested_path:, memo:, renamed_params:, route_params:)
|
|
80
|
+
return unless @include_missing || (passed_params.respond_to?(:key?) && passed_params.key?(declared_param))
|
|
93
81
|
|
|
82
|
+
memo_key = build_memo_key(params_nested_path, declared_param, renamed_params)
|
|
83
|
+
passed_param = passed_params[declared_param]
|
|
84
|
+
|
|
85
|
+
memo[memo_key] = passed_param || handle_passed_param(nested_path_for(params_nested_path, declared_param), route_params:) do
|
|
86
|
+
passed_param
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_memo_key(params_nested_path, declared_param, renamed_params)
|
|
91
|
+
renamed_param_name = renamed_params[nested_path_for(params_nested_path, declared_param)]
|
|
94
92
|
param = renamed_param_name || declared_param
|
|
95
93
|
@stringify ? param.to_s : param.to_sym
|
|
96
94
|
end
|
|
97
95
|
|
|
98
|
-
def
|
|
96
|
+
def nested_path_for(parent_path, key)
|
|
97
|
+
parent_path + [key.to_s]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def handle_passed_param(params_nested_path, route_params:, has_passed_children: false)
|
|
99
101
|
return yield if has_passed_children
|
|
100
102
|
|
|
101
|
-
key = params_nested_path
|
|
103
|
+
key = params_nested_path.first
|
|
102
104
|
key += "[#{params_nested_path[1..].join('][')}]" if params_nested_path.size > 1
|
|
103
105
|
|
|
104
106
|
type = route_params.dig(key, :type)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if type == 'Hash' &&
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
Set.new
|
|
113
|
-
else
|
|
114
|
-
yield
|
|
115
|
-
end
|
|
107
|
+
return yield if type.nil?
|
|
108
|
+
|
|
109
|
+
return {} if type == 'Hash' && route_params.keys.none? { |k| k != key && k.start_with?("#{key}[") }
|
|
110
|
+
return [] if type == 'Array' || (type.start_with?('[') && !type.include?(','))
|
|
111
|
+
return Set.new if type == 'Set' || type.start_with?('#<Set', 'Set')
|
|
112
|
+
|
|
113
|
+
yield
|
|
116
114
|
end
|
|
117
115
|
end
|
|
118
116
|
end
|
data/lib/grape/dsl/callbacks.rb
CHANGED
|
@@ -9,9 +9,15 @@ module Grape
|
|
|
9
9
|
# after: execute the given block after the endpoint code has run except in unsuccessful
|
|
10
10
|
# finally: execute the given block after the endpoint code even if unsuccessful
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
{
|
|
13
|
+
before: :befores,
|
|
14
|
+
before_validation: :before_validations,
|
|
15
|
+
after_validation: :after_validations,
|
|
16
|
+
after: :afters,
|
|
17
|
+
finally: :finallies
|
|
18
|
+
}.each do |method_name, plural_key|
|
|
19
|
+
define_method method_name do |&block|
|
|
20
|
+
inheritable_setting.namespace_stackable[plural_key] = block
|
|
15
21
|
end
|
|
16
22
|
end
|
|
17
23
|
end
|