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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +128 -0
- data/CONTRIBUTING.md +16 -0
- data/README.md +208 -8
- data/UPGRADING.md +756 -6
- data/lib/grape/api/instance.rb +40 -33
- data/lib/grape/content_types.rb +47 -1
- data/lib/grape/dry_types.rb +1 -1
- data/lib/grape/dsl/callbacks.rb +3 -9
- data/lib/grape/dsl/declared.rb +4 -4
- data/lib/grape/dsl/desc.rb +18 -8
- data/lib/grape/dsl/entity.rb +37 -19
- data/lib/grape/dsl/helpers.rb +3 -3
- data/lib/grape/dsl/inside_route.rb +20 -8
- data/lib/grape/dsl/middleware.rb +3 -3
- data/lib/grape/dsl/parameters.rb +54 -28
- data/lib/grape/dsl/request_response.rb +37 -39
- data/lib/grape/dsl/rescue_options.rb +6 -5
- data/lib/grape/dsl/routing.rb +116 -49
- data/lib/grape/dsl/settings.rb +1 -1
- data/lib/grape/dsl/validations.rb +3 -3
- data/lib/grape/dsl/version_options.rb +3 -3
- data/lib/grape/endpoint/options.rb +13 -7
- data/lib/grape/endpoint.rb +141 -118
- data/lib/grape/env.rb +7 -0
- data/lib/grape/error_formatter/base.rb +6 -5
- data/lib/grape/error_formatter.rb +6 -2
- data/lib/grape/exceptions/error_response.rb +4 -1
- data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
- data/lib/grape/exceptions/validation.rb +9 -8
- data/lib/grape/formatter/json.rb +1 -0
- data/lib/grape/formatter/serializable_hash.rb +1 -0
- data/lib/grape/locale/en.yml +1 -0
- data/lib/grape/middleware/auth/dsl.rb +5 -33
- data/lib/grape/middleware/error.rb +153 -46
- data/lib/grape/middleware/formatter.rb +95 -26
- data/lib/grape/middleware/precomputed_content_types.rb +12 -7
- data/lib/grape/middleware/stack.rb +13 -4
- data/lib/grape/middleware/versioner/base.rb +3 -9
- data/lib/grape/middleware/versioner/path.rb +49 -7
- data/lib/grape/namespace.rb +3 -2
- data/lib/grape/path.rb +8 -69
- data/lib/grape/precompiled_json.rb +50 -0
- data/lib/grape/request.rb +36 -4
- data/lib/grape/router/base_route.rb +47 -10
- data/lib/grape/router/greedy_route.rb +5 -1
- data/lib/grape/router/pattern/path.rb +78 -0
- data/lib/grape/router/pattern.rb +60 -14
- data/lib/grape/router/route.rb +62 -10
- data/lib/grape/router.rb +100 -57
- data/lib/grape/serve_stream/file_body.rb +6 -0
- data/lib/grape/serve_stream/stream_response.rb +6 -0
- data/lib/grape/util/api_description.rb +10 -1
- data/lib/grape/util/cache.rb +21 -2
- data/lib/grape/util/deep_freeze.rb +1 -2
- data/lib/grape/util/freeze_on_new.rb +20 -0
- data/lib/grape/util/inheritable_setting.rb +751 -37
- data/lib/grape/util/media_type.rb +10 -3
- data/lib/grape/util/path_normalizer.rb +12 -9
- data/lib/grape/util/registry.rb +12 -2
- data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
- data/lib/grape/util/stackable_values.rb +32 -6
- data/lib/grape/validations/attributes_iterator.rb +21 -4
- data/lib/grape/validations/contract_scope.rb +8 -7
- data/lib/grape/validations/oneof_collector.rb +6 -10
- data/lib/grape/validations/params_documentation.rb +2 -2
- data/lib/grape/validations/params_scope.rb +66 -26
- data/lib/grape/validations/single_attribute_iterator.rb +5 -3
- data/lib/grape/validations/types/array_coercer.rb +4 -6
- data/lib/grape/validations/types/custom_type_coercer.rb +7 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +3 -1
- data/lib/grape/validations/types/json.rb +1 -3
- data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
- data/lib/grape/validations/types/primitive_coercer.rb +12 -9
- data/lib/grape/validations/types/variant_collection_coercer.rb +8 -3
- data/lib/grape/validations/types.rb +5 -5
- data/lib/grape/validations/validations_spec.rb +14 -4
- data/lib/grape/validations/validators/base.rb +4 -7
- data/lib/grape/validations/validators/coerce_validator.rb +8 -2
- data/lib/grape/validations/validators/contract_scope_validator.rb +2 -1
- data/lib/grape/validations/validators/length_validator.rb +4 -2
- data/lib/grape/validations/validators/oneof_validator.rb +2 -0
- data/lib/grape/validations/validators/same_as_validator.rb +1 -0
- data/lib/grape/version.rb +2 -1
- data/lib/grape.rb +23 -2
- metadata +10 -9
- data/lib/grape/middleware/deprecated_options_hash_access.rb +0 -19
- data/lib/grape/util/base_inheritable.rb +0 -43
- data/lib/grape/util/inheritable_values.rb +0 -33
- data/lib/grape/util/reverse_stackable_values.rb +0 -15
data/UPGRADING.md
CHANGED
|
@@ -1,6 +1,745 @@
|
|
|
1
1
|
Upgrading Grape
|
|
2
2
|
===============
|
|
3
3
|
|
|
4
|
+
### Upgrading to >= 4.0.0
|
|
5
|
+
|
|
6
|
+
#### A positional options Hash is no longer accepted by `auth`, `http_basic` or `desc`
|
|
7
|
+
|
|
8
|
+
Deprecated in 3.3 ([#2723](https://github.com/ruby-grape/grape/pull/2723), [#2728](https://github.com/ruby-grape/grape/pull/2728)). These take keyword arguments:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
# Before
|
|
12
|
+
auth :custom, { realm: 'r', opaque: 'o' }
|
|
13
|
+
http_basic({ realm: 'API' })
|
|
14
|
+
desc 'Get users', { detail: 'Returns every user' }
|
|
15
|
+
|
|
16
|
+
# After
|
|
17
|
+
auth :custom, realm: 'r', opaque: 'o'
|
|
18
|
+
http_basic realm: 'API'
|
|
19
|
+
desc 'Get users', detail: 'Returns every user'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
A leftover positional Hash now raises `ArgumentError` rather than warning. A call that already used bare keyword syntax, or a block, is unaffected.
|
|
23
|
+
|
|
24
|
+
#### `http_digest` is removed
|
|
25
|
+
|
|
26
|
+
`Grape::Middleware::Auth::DSL#http_digest` is gone. Calling it now raises `NoMethodError` while the API class is being defined.
|
|
27
|
+
|
|
28
|
+
Nothing it could reach has existed since **2.0.0**, which removed `Rack::Auth::Digest` along with Grape's `:http_digest` strategy ([#2361](https://github.com/ruby-grape/grape/pull/2361)) after Rack 3 dropped digest authentication. The method survived that removal and kept recording its settings happily, so an API declaring `http_digest` still booted — and then raised `Grape::Exceptions::UnknownAuthStrategy` on the *first request*, from inside the middleware build, as an uncaught exception rather than a response. Failing while the class is defined is the point of removing it.
|
|
29
|
+
|
|
30
|
+
**If you registered your own `:http_digest` strategy**, it still works; call `auth` directly:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
Grape::Middleware::Auth::Strategies.add(:http_digest, MyDigestStrategy, ->(settings) { [settings[:realm]] })
|
|
34
|
+
|
|
35
|
+
class API < Grape::API
|
|
36
|
+
auth :http_digest, realm: 'API Authorization', opaque: 'secret' do |username|
|
|
37
|
+
# ...
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The removed method supplied two defaults that `auth` does not, so pass them explicitly if you were relying on them: `realm` defaulted to `'API Authorization'`, and `opaque` to `'secret'` (nested inside `realm` when `realm` was itself a Hash).
|
|
43
|
+
|
|
44
|
+
#### A `rescue_from` handler can no longer return or throw a Hash
|
|
45
|
+
|
|
46
|
+
Deprecated in 3.3. A handler that returned `{ message:, status:, headers: }` was read as an error response. Say what you mean instead:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# Before
|
|
50
|
+
rescue_from :all do |e|
|
|
51
|
+
{ message: e.message, status: 500, headers: {} }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# After
|
|
55
|
+
rescue_from :all do |e|
|
|
56
|
+
error!(e.message, 500)
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
A handler that still returns such a Hash no longer has it interpreted: the response is an invalid one, which Grape answers with its framework default rather than with the status the Hash carried.
|
|
61
|
+
|
|
62
|
+
#### Middleware `Options` no longer answer `[]`, and the `DEFAULT_OPTIONS` constants are gone
|
|
63
|
+
|
|
64
|
+
Deprecated in 3.3, when middleware options moved to per-class `Options` `Data` value objects. `Grape::Middleware::Error::DEFAULT_OPTIONS`, `Grape::Middleware::Formatter::DEFAULT_OPTIONS` and `Grape::Middleware::Versioner::Base::DEFAULT_OPTIONS` are removed, as is Hash-style access on the `Options` objects themselves:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# Before
|
|
68
|
+
middleware.config[:default_format]
|
|
69
|
+
Grape::Middleware::Formatter::DEFAULT_OPTIONS[:default_format]
|
|
70
|
+
|
|
71
|
+
# After
|
|
72
|
+
middleware.config.default_format
|
|
73
|
+
Grape::Middleware::Formatter::Options.new.default_format
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`Grape::Middleware::Base` still supports a plain `DEFAULT_OPTIONS` Hash on middleware that declares no `Options` class, so third-party middleware written against that path is unaffected.
|
|
77
|
+
|
|
78
|
+
#### `Grape::Router.normalize_path` is removed
|
|
79
|
+
|
|
80
|
+
Deprecated in 3.3. Use `Grape::Util::PathNormalizer.call`, which is where the implementation has lived since.
|
|
81
|
+
|
|
82
|
+
#### Custom validators read `@options`, not `@option`
|
|
83
|
+
|
|
84
|
+
`Grape::Validations::Validators::Base` kept `@option` as an alias of `@options` from 3.2 with a note to drop it at the next major. A custom validator reading `@option` now sees `nil`:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# Before
|
|
88
|
+
def validate_param!(attr_name, params)
|
|
89
|
+
return if params[attr_name].length <= @option
|
|
90
|
+
# ...
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# After
|
|
94
|
+
def validate_param!(attr_name, params)
|
|
95
|
+
return if params[attr_name].length <= @options
|
|
96
|
+
# ...
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`@options`, `option_value` and the rest of the documented custom-validator surface are unchanged.
|
|
101
|
+
#### `Grape::ErrorFormatter.formatter_for` answers `nil` for an unregistered format
|
|
102
|
+
|
|
103
|
+
It used to take the API's `default_error_formatter` as a third argument and end in `default_error_formatter || Grape::ErrorFormatter::Txt` — a global registry implementing the caller's fallback policy. It is now a lookup, the same shape as `Grape::Parser.parser_for`:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
Grape::ErrorFormatter.formatter_for(:json) # => Grape::ErrorFormatter::Json
|
|
107
|
+
Grape::ErrorFormatter.formatter_for(:unknown) # => nil, was Grape::ErrorFormatter::Txt
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Passing a third argument now raises `ArgumentError`. The fallback moved to `Grape::Middleware::Error`, which applies the API's `default_error_formatter` and resolves that to `Grape::ErrorFormatter::Txt` when the API set none.
|
|
111
|
+
|
|
112
|
+
Error responses are unchanged, including the precedence: a formatter registered for the requested format still wins over the API's `default_error_formatter`, which applies to formats that have none of their own.
|
|
113
|
+
|
|
114
|
+
Two things follow at definition time, where the lookup's fallback used to hide them.
|
|
115
|
+
|
|
116
|
+
`default_error_formatter` names an error formatter, so a name nothing is registered under is now an error rather than a silent `Txt`:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
default_error_formatter :jsonn
|
|
120
|
+
# => Grape::Exceptions::UnknownErrorFormatter: unknown error formatter: jsonn
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`format` names a *format*, and a format with no error formatter of its own is ordinary, so it still succeeds — but the stored setting is now `nil` rather than `Grape::ErrorFormatter::Txt`:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
content_type :xls, 'application/vnd.ms-excel'
|
|
127
|
+
format :xls
|
|
128
|
+
MyAPI.default_error_formatter # => nil, was Grape::ErrorFormatter::Txt
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The middleware supplies `Txt` at render time, so error responses are unchanged; the setting is only visible if you read it back.
|
|
132
|
+
|
|
133
|
+
#### `error_formatter` now requires a formatter
|
|
134
|
+
|
|
135
|
+
`error_formatter` takes the formatter positionally or as `with:`. Called with neither, it used to register `nil` for that format:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
error_formatter :json # no formatter given
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`Grape::ErrorFormatter.formatter_for` hands a registered value back as-is, so the format resolved to no formatter at all — and every error response in that format then failed to render and came back as the failsafe `500 Internal Server Error` in `text/plain`, whatever status and message the API had asked for.
|
|
142
|
+
|
|
143
|
+
That call now raises `ArgumentError` when the API is defined, rather than on the first error the API tries to render. Pass the formatter:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
error_formatter :json, with: MyErrorFormatter
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
An API that already passes one is unaffected, and a format with no `error_formatter` of its own keeps falling back to `default_error_formatter` and then to `Grape::ErrorFormatter::Txt`.
|
|
150
|
+
|
|
151
|
+
#### `grape/testing` is no longer loaded unless you require it
|
|
152
|
+
|
|
153
|
+
`Grape::Testing` has been documented as opt-in since 3.3 — "intended for test environments only and is not loaded by default" — but it was never excluded from Grape's eager load, so `require 'grape'` installed it anyway. It extends `Grape::Endpoint` with `before_each` / `reset_before_each` and prepends a wrapper around `Grape::Endpoint#run` that every request goes through.
|
|
154
|
+
|
|
155
|
+
It is now excluded, so the documented behaviour is the actual one. If your suite calls `Grape::Endpoint.before_each` without requiring the module first, it will now raise `NoMethodError`. Add the require the 3.3 note already asked for to your test helper:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
require 'grape/testing'
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Nothing else changes: the module's API is the same, and referencing the `Grape::Testing` constant still autoloads it.
|
|
162
|
+
|
|
163
|
+
#### The versioner's `pattern` option has been removed
|
|
164
|
+
|
|
165
|
+
`Grape::Middleware::Versioner::Base` accepted a `pattern` option, which `Versioner::Path` matched the candidate version segment against before recording it. It dates from the original versioner (2010), where it was the only way to decide whether the first path segment was a version — there was no declared version list yet.
|
|
166
|
+
|
|
167
|
+
`versions:` took that job over: a segment is checked against the versions the API declared, and an unrecognized one answers 404. `pattern` was never connected to the `version` DSL that arrived later — `DSL::Routing#version` does not accept a `pattern:` keyword and `Endpoint#build_stack` never passed one — so it could only be reached by inserting the middleware yourself:
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
use Grape::Middleware::Versioner::Path, versions: %w[v1], pattern: /v.+/
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
which also meant forgoing what `version` does for routing. That now raises `ArgumentError: unknown keyword: :pattern`, at the point the middleware is built rather than on a request.
|
|
174
|
+
|
|
175
|
+
`Grape::Middleware::Versioner::Base::DEFAULT_OPTIONS` no longer carries a `:pattern` key, and `Versioner::Base#pattern` is gone. An API that declares its version through `version 'v1', using: :path` is unaffected — it never had a pattern.
|
|
176
|
+
|
|
177
|
+
The one behaviour that disappears with it: a hand-wired versioner could use `pattern` to say "this segment is not a version at all", leaving `api.version` unset and letting the request through, as distinct from "unknown version", which answers 404. Nothing in the `version` DSL exposed that distinction, and `versions:` cannot express it. If you relied on it, match the segment yourself in a small middleware ahead of the versioner rather than through this option.
|
|
178
|
+
|
|
179
|
+
#### An error response carries a backtrace only when one was asked for
|
|
180
|
+
|
|
181
|
+
Every error response used to materialize the raised exception's backtrace, even though the built-in error formatters render it only under `rescue_from ..., backtrace: true`. It is now assembled only when that option is set, since `Exception#backtrace` builds an Array of location Strings on every call and a Grape error is raised deep inside the request stack.
|
|
182
|
+
|
|
183
|
+
`Grape::Exceptions::ErrorResponse#backtrace` is therefore an empty Array on the payload handed to an error formatter unless the API asked for backtraces. The built-in formatters are unaffected — they already guarded on the `include_backtrace:` argument they are passed.
|
|
184
|
+
|
|
185
|
+
A custom error formatter that reads `error.backtrace` while ignoring that argument will now see an empty Array. Read it from the exception instead, which still travels on the payload:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
# before
|
|
189
|
+
error.backtrace
|
|
190
|
+
|
|
191
|
+
# after — equivalent regardless of the rescue options
|
|
192
|
+
error.original_exception&.backtrace || []
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`Grape::Exceptions::ErrorResponse.from_exception` no longer stores a backtrace either, for the same reason; `original_exception` is set, so nothing is lost.
|
|
196
|
+
|
|
197
|
+
#### An endpoint that declares helpers is served from a subclass of `Grape::Endpoint`
|
|
198
|
+
|
|
199
|
+
Helpers were included into the singleton class of the per-request copy of an endpoint, which meant every request built a fresh singleton class and re-ran `Module#include`. They are now included once, when the API is compiled, into a subclass owned by that endpoint, and each request copies an instance of it.
|
|
200
|
+
|
|
201
|
+
The endpoint exposed as `env['api.endpoint']` (and handed to `before`/`after` filters, `rescue_from` blocks and middleware) is therefore an instance of that subclass rather than of `Grape::Endpoint` itself, for any endpoint whose scope declares `helpers`. Endpoints with no helpers are unaffected.
|
|
202
|
+
|
|
203
|
+
Inheritance-based checks are unchanged:
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
endpoint.is_a?(Grape::Endpoint) # => true, as before
|
|
207
|
+
endpoint.kind_of?(Grape::Endpoint) # => true, as before
|
|
208
|
+
Grape::Endpoint === endpoint # => true, as before
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Exact-class checks are not, and have no equivalent — use `is_a?`:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
# before => true, now => false
|
|
215
|
+
endpoint.instance_of?(Grape::Endpoint)
|
|
216
|
+
endpoint.class == Grape::Endpoint
|
|
217
|
+
|
|
218
|
+
# before => "Grape::Endpoint", now => "Grape::Endpoint(helpers)"
|
|
219
|
+
endpoint.class.name
|
|
220
|
+
endpoint.class.to_s
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The subclass carries a temporary name (`Module#set_temporary_name`), so it never answers `nil` to `#name` or renders as a bare `#<Class:0x...>` address. Code that logs or groups by `endpoint.class.name` — an error tracker keying on it, say — will see `Grape::Endpoint(helpers)` for these endpoints, and should match on `is_a?` if it needs to treat both kinds alike.
|
|
224
|
+
#### The `desc` `default` key is renamed to `default_response`
|
|
225
|
+
|
|
226
|
+
grape-swagger reads a route's default response as `route.default_response`, a name Grape never defined — the `desc` DSL wrote the key as `default`, and the reader for it resolved to `Hash#default` on the route's options bag, so it answered `nil` for every route. A default response declared the way the README documented it therefore never reached the generated OpenAPI document.
|
|
227
|
+
|
|
228
|
+
The key, the `desc` block method and the route reader are all named `default_response` now. `default` still works everywhere it did, and warns:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
# deprecated
|
|
232
|
+
desc 'Delete a widget' do
|
|
233
|
+
default code: 'default', message: 'unexpected error'
|
|
234
|
+
end
|
|
235
|
+
desc 'Delete a widget', default: { code: 'default' }
|
|
236
|
+
route.default
|
|
237
|
+
|
|
238
|
+
# preferred
|
|
239
|
+
desc 'Delete a widget' do
|
|
240
|
+
default_response code: 'default', message: 'unexpected error'
|
|
241
|
+
end
|
|
242
|
+
desc 'Delete a widget', default_response: { code: 'default' }
|
|
243
|
+
route.default_response
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
A `desc 'x', default: ...` keyword is stored under `:default_response`, so a route carries one key under one name. Code reading `route.options[:default]` directly — rather than through the reader — has to read `route.options[:default_response]` instead.
|
|
247
|
+
#### A group's type check is now the same for `requires` and `optional`
|
|
248
|
+
|
|
249
|
+
The rule that a group declaration needs a type (`Array`, `Hash`, `JSON` or `Array[JSON]`) was implemented twice — once in `ParamsScope#new_scope` for `requires`, once inline in `optional` — and the two copies had drifted. `optional` read the type before merging in the attributes of an enclosing `with`, so a group type supplied by `with` was invisible to it:
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
params do
|
|
253
|
+
with(type: Hash) do
|
|
254
|
+
requires(:a) { requires :b, type: String } # accepted
|
|
255
|
+
optional(:a) { optional :b, type: String } # raised MissingGroupType
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Both are accepted now. A group with no type from either the declaration or an enclosing `with` still raises `Grape::Exceptions::MissingGroupType`, and an unsupported type still raises `Grape::Exceptions::UnsupportedGroupType`.
|
|
261
|
+
|
|
262
|
+
One case stops raising. `optional :a, using: SomeEntity do ... end` — a block *and* `using:` — raised `MissingGroupType` before; it now ignores the block and documents the entity's params, which is what `requires` has always done with that combination. If you have such a declaration, the block was never going to be applied; delete it or drop `using:`.
|
|
263
|
+
#### The `presence` option of `requires` and `optional` is deprecated
|
|
264
|
+
|
|
265
|
+
`presence` is how the params DSL tells the validation pipeline that a parameter is required. It travelled as a key written into the same options Hash that carries an API's own options, so an API could set it — and got opposite results depending on which method it was passed to, because `requires` overwrote it while nothing overwrote it for `optional`:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
requires :a, presence: false # ignored: :a is still required
|
|
269
|
+
optional :a, presence: true # honoured: :a is actually required
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Both still resolve exactly as they did, and now emit a deprecation warning. Declare the parameter with `requires` to make it required and `optional` to make it optional; the option will be ignored outright in a future release.
|
|
273
|
+
|
|
274
|
+
#### A `with` block's `message` now reaches the presence validator
|
|
275
|
+
|
|
276
|
+
The presence entry for a `requires` was built before the attributes of an enclosing `with` were merged in, so a group-level `message:` never reached it. It does now:
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
params do
|
|
280
|
+
with(message: 'custom missing') do
|
|
281
|
+
requires :a
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# before: "a is missing"
|
|
286
|
+
# now: "a custom missing"
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
An API that sets `message:` on a `with` block and relies on the generic wording for missing parameters will see its own message instead.
|
|
290
|
+
|
|
291
|
+
#### `refresh_already_mounted` is no longer a `mount` option
|
|
292
|
+
|
|
293
|
+
`mount` accepted an undocumented `refresh_already_mounted` option that replaced any endpoint already mounted for the same base API instead of adding a second one. It exists for Grape's own re-mounting machinery: when a class-level method runs after a `mount`, `Grape::API.refresh_mount_step` replays that mount, and replaying it naively would duplicate the endpoint.
|
|
294
|
+
|
|
295
|
+
That decision belongs to the replay, not to the call, so the endpoint replacement moved into the private `refresh_mounted_api` which is the only caller that ever wanted it. Nothing changes for the re-mounting itself.
|
|
296
|
+
|
|
297
|
+
Passing the option to `mount` still works and now warns; it will be ignored in a future release. There is no replacement, because an API has no reason to ask for it — mounting the same app twice on purpose is still two endpoints.
|
|
298
|
+
|
|
299
|
+
#### A `mount` target is recognised as a mapping only when it is a `Hash`
|
|
300
|
+
|
|
301
|
+
`mount` decided between "a Hash of app => path" and "a bare app mounted at /" with `mounts.respond_to?(:each_pair)`. A `Struct` and an `OpenStruct` answer that too, and neither can express an app => path mapping — their keys are member names — so a Rack app that happened to be one was read as a mapping and silently mounted its own field value as a path:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
RackStruct = Struct.new(:name) do
|
|
305
|
+
def call(env) = [200, {}, ['ok']]
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
mount RackStruct.new('x')
|
|
309
|
+
|
|
310
|
+
# before: mounted at "/x", because :name => 'x' was read as app => path
|
|
311
|
+
# now: mounted at "/", as the Rack app it is
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
The test is now `mounts.is_a?(Hash)`. Every Hash-like people pass — `HashWithIndifferentAccess`, `Hashie::Mash`, `ActiveSupport::OrderedOptions` — subclasses `Hash` and is unaffected.
|
|
315
|
+
|
|
316
|
+
#### `use`, `helpers`, `rescue_from` and other registrations no longer reach routes defined above them
|
|
317
|
+
|
|
318
|
+
A route captures the middleware, helpers, callbacks and rescue handlers registered above it. That was already true most of the time, but not always: `Grape::Util::InheritableSetting#point_in_time_copy` copied a scope's stackable store and its rescue-handler maps shallowly, so the nested Arrays and Hashes stayed shared with the scope. A registration added *after* an endpoint was defined therefore still reached that endpoint — but only when the key already held at least one registration when the endpoint was defined, since otherwise the scope allocated a fresh store only for itself.
|
|
319
|
+
|
|
320
|
+
The outcome depended on something the API never expressed:
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
class A < Grape::API
|
|
324
|
+
use Middleware1
|
|
325
|
+
get('/x') { } # endpoint defined here
|
|
326
|
+
use Middleware2 # applied to GET /x
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
class B < Grape::API
|
|
330
|
+
get('/x') { } # endpoint defined here
|
|
331
|
+
use Middleware2 # NOT applied to GET /x
|
|
332
|
+
end
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
`A` and `B` state the same thing and behaved differently. Both now behave like `B`. The same held for `rescue_from` declared below a route.
|
|
336
|
+
|
|
337
|
+
**What can break.** An API that declares `use` (or `helpers`, a filter such as `before`, or `rescue_from`) below its routes and relies on it applying to them. That arrangement only ever worked when an earlier registration for the same key happened to seed the stack, so it was never dependable, but code written against it will now see the middleware or helper silently not run.
|
|
338
|
+
|
|
339
|
+
**The fix is to move the registration above the routes it should cover**, which is where Grape's documentation has always placed it:
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
class A < Grape::API
|
|
343
|
+
use Middleware1
|
|
344
|
+
use Middleware2
|
|
345
|
+
get('/x') { }
|
|
346
|
+
end
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Nothing changes for the ordinary arrangement — registrations declared before a route, or inherited from an enclosing namespace or a mounting API, still apply exactly as before, including values an enclosing scope gains after the nested scope was created.
|
|
350
|
+
#### `rescue_from :grape_exceptions` now outranks a catch-all class handler
|
|
351
|
+
|
|
352
|
+
`rescue_from :grape_exceptions` is an opt-in to keep Grape's own errors rendering with their own status — a validation failure answers `400` rather than whatever the application's catch-all returns.
|
|
353
|
+
|
|
354
|
+
It only ever worked against `rescue_from :all`. Written as a class instead, a catch-all is a *registered* handler, which `Grape::Middleware::Error` consults first, and Grape's exceptions are `StandardError`s — so the opt-in was silently inert:
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
rescue_from StandardError do
|
|
358
|
+
error!('server error', 500)
|
|
359
|
+
end
|
|
360
|
+
rescue_from :grape_exceptions
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
| request | before | 4.0 |
|
|
364
|
+
| --- | --- | --- |
|
|
365
|
+
| fails parameter validation | **500** `server error` | `400` |
|
|
366
|
+
| raises an application error | `500` `server error` | unchanged |
|
|
367
|
+
|
|
368
|
+
**What can break.** An API that registers a catch-all as a class *and* opts into `:grape_exceptions` will now answer Grape's own status for Grape's own errors, where it previously answered the catch-all's. That is what the opt-in asks for, so the change makes the two spellings agree — but a client or test asserting the catch-all's status for a validation failure will see the new one.
|
|
369
|
+
|
|
370
|
+
Precedence is unchanged in every other case. A handler registered for a specific Grape exception class is more precise than the opt-in and still wins:
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
rescue_from Grape::Exceptions::ValidationErrors do
|
|
374
|
+
error!('unprocessable', 422) # still runs
|
|
375
|
+
end
|
|
376
|
+
rescue_from StandardError { ... }
|
|
377
|
+
rescue_from :grape_exceptions
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Application errors still reach the catch-all, `rescue_from :all` behaves as before, and `Grape::Exceptions::InvalidVersionHeader` is still never rescued, so version cascading keeps working. An API that does not use `rescue_from :grape_exceptions` is unaffected.
|
|
381
|
+
|
|
382
|
+
#### `redirect` renders its default message as plain text
|
|
383
|
+
|
|
384
|
+
The `redirect` API set the content type to `text/plain`, but delegated body rendering to the formatter. With an API defaulting to JSON, a `redirect` would render the body as JSON with a `text/plain` content-type header.
|
|
385
|
+
|
|
386
|
+
```ruby
|
|
387
|
+
class API < Grape::API
|
|
388
|
+
format :json
|
|
389
|
+
get('/r') { redirect '/there' }
|
|
390
|
+
end
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
Content-Type: text/plain
|
|
395
|
+
|
|
396
|
+
"This resource has been moved temporarily to /there."
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Grape now renders the message it generates with the txt formatter, so the body is the plain sentence the content type claims:
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
Content-Type: text/plain
|
|
403
|
+
|
|
404
|
+
This resource has been moved temporarily to /there.
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**What can break.** Code that parses a redirect body — `JSON.parse(response.body)` on a redirect succeeded before and now raises — or a test asserting on the encoded form. On a JSON API the `Location` header, the status and the `Content-Type` are unchanged, so a client that follows the redirect is unaffected.
|
|
408
|
+
|
|
409
|
+
On an API whose formatter cannot serialize a String, such as `format :xml`, `redirect` did not work at all: the formatter raised, and the response was a `500` carrying an error document and no `Location` header. Those APIs now get the `302` and the `Location` they always should have.
|
|
410
|
+
|
|
411
|
+
This applies only to the message Grape generates. A body you pass yourself is still rendered by the API's formatter, unchanged:
|
|
412
|
+
|
|
413
|
+
```ruby
|
|
414
|
+
redirect '/there', body: { message: 'moved' } # still {"message":"moved"} on a JSON API
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
#### Path params are tagged UTF-8 instead of ASCII-8BIT
|
|
418
|
+
|
|
419
|
+
Params captured from the request path — `route_param`, `:id`-style segments, splats — now come back tagged `UTF-8`. They used to carry the `ASCII-8BIT` encoding of Rack's `PATH_INFO`, because Mustermann decodes the path against that raw string and nothing re-tagged the result. Query and body params were already `UTF-8`, since Rack tags those itself.
|
|
420
|
+
|
|
421
|
+
**Why UTF-8.** Nothing obliges a client to send it — HTTP treats the request target as octets, and Rack's SPEC has CGI keys carry non-ASCII as `ASCII-8BIT`. UTF-8 is a convention rather than a guarantee. But it is the convention Rack itself already applies to everything *except* the path: `Rack::QueryParser#unescape` decodes the query string and form bodies with `URI.decode_www_form_component(string, Encoding::UTF_8)`, which tags the result without validating it.
|
|
422
|
+
|
|
423
|
+
One request carrying the same invalid octets in three places, before this change:
|
|
424
|
+
|
|
425
|
+
| source | encoding | `valid_encoding?` |
|
|
426
|
+
| --- | --- | --- |
|
|
427
|
+
| query — `?q=%C3%28` | `UTF-8` | `false` |
|
|
428
|
+
| form body — `form=%C3%28` | `UTF-8` | `false` |
|
|
429
|
+
| path — `/%C3%28` | **`ASCII-8BIT`** | `true` |
|
|
430
|
+
|
|
431
|
+
The bytes are equally malformed in all three; only the label differed. The path reads `true` merely because `ASCII-8BIT` considers every byte sequence valid. So this change is not Grape adopting an outside convention — it is Grape agreeing with the library handing it the request. The path param was the odd one out only because Mustermann decodes against `PATH_INFO` directly and never had Rack's `unescape` applied to it.
|
|
432
|
+
|
|
433
|
+
Grape does exactly what Rack does: re-tag, do not validate. The bytes are untouched, so octets that are *not* UTF-8 stay detectably invalid rather than being scrubbed into something the client never sent. (Rails takes the same approach for path captures — `ActionDispatch::Journey::Router` force-encodes each one to UTF-8 after unescaping.)
|
|
434
|
+
|
|
435
|
+
**What this fixes.** An API's own declarations used to disagree with themselves depending on where a value arrived from — a binary string never equals the UTF-8 literal it was written as:
|
|
436
|
+
|
|
437
|
+
```ruby
|
|
438
|
+
params { requires :id, type: String, values: ['café'] }
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
| request | before | 4.0 |
|
|
442
|
+
| --- | --- | --- |
|
|
443
|
+
| `GET /?id=café` (query) | `200` | unchanged |
|
|
444
|
+
| `GET /café` (path) | `400 "id does not have a valid value"` | `200` |
|
|
445
|
+
|
|
446
|
+
The same held for `same_as`, `except_values`, and any comparison an endpoint made against a non-ASCII literal. Serialization was affected too: a non-ASCII path param rendered into a JSON response emitted an encoding warning from the `json` gem, and is slated to raise there in json 3.0.
|
|
447
|
+
|
|
448
|
+
**What can break.** Only the encoding tag changes; the bytes are untouched, and an invalid byte sequence stays invalid rather than being scrubbed. Comparisons against pure-ASCII strings are unaffected. Code that relied on a path param being binary — concatenating one with genuinely binary data, for instance — can now raise `Encoding::CompatibilityError`, and should call `.b` on the param to opt back into binary:
|
|
449
|
+
|
|
450
|
+
```ruby
|
|
451
|
+
params[:id].b + binary_blob
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
An application that worked around the old behavior with its own `force_encoding(Encoding::UTF_8)` needs no change; that call is now a no-op.
|
|
455
|
+
|
|
456
|
+
**Restoring the old behavior wholesale.** If enough code depends on binary path params that patching each site is impractical, a `before` filter at the top of your root API re-tags them all back. `env['grape.routing_args']` holds exactly the params that came from the path, so query and body params are left alone, and `before` runs ahead of validation, so declarations see the binary strings too:
|
|
457
|
+
|
|
458
|
+
```ruby
|
|
459
|
+
class API < Grape::API
|
|
460
|
+
before do
|
|
461
|
+
env['grape.routing_args']&.each_key do |key|
|
|
462
|
+
next if key == :route_info
|
|
463
|
+
|
|
464
|
+
value = params[key]
|
|
465
|
+
params[key] = value.b if value.is_a?(String)
|
|
466
|
+
params[key] = value.map(&:b) if value.is_a?(Array)
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# ... mounts and routes
|
|
471
|
+
end
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Declared in the root API, this covers mounted APIs too. Treat it as a migration aid rather than a permanent setting: it restores the inconsistency this change fixes, so `values: ['café']` will keep rejecting `GET /café` while accepting `?id=café`.
|
|
475
|
+
#### A failed error rendering answers 500 instead of escaping the middleware stack
|
|
476
|
+
|
|
477
|
+
When Grape could not render an error response — an error formatter handed a payload it cannot serialize, most often — the exception escaped every middleware above Grape and reached the application server. Rendering runs inside `Grape::Middleware::Error#call!`'s own `rescue` clause, so that clause did not cover it.
|
|
478
|
+
|
|
479
|
+
Grape now answers `500` instead: first retrying the API's format with the framework's own `Internal Server Error` message, then falling back to a bare `text/plain` body if even that cannot be rendered.
|
|
480
|
+
|
|
481
|
+
This mirrors what `ActionDispatch::ShowExceptions#render_exception` does in Rails: try the application's own error rendering, and fall back to a bare `500 Internal Server Error` in `text/plain` when that rendering is itself broken.
|
|
482
|
+
|
|
483
|
+
**What can break.** Code that observed these exceptions by letting them propagate — a test asserting `expect { get '/' }.to raise_error`, most directly — no longer sees them raised. The exception is published on the rack env instead, under both Grape's own key and the conventional one that error trackers read:
|
|
484
|
+
|
|
485
|
+
```ruby
|
|
486
|
+
env[Grape::Env::RACK_EXCEPTION] # 'rack.exception' — what trackers collect
|
|
487
|
+
env[Grape::Env::GRAPE_EXCEPTION] # 'grape.exception' — same object, Grape's key
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
An error tracker mounted as Rack middleware above Grape therefore keeps reporting these with no change on your side: sentry-ruby, for one, collects `env['rack.exception'] || env['sinatra.error']` for exactly this case — an exception that was handled rather than raised. The failure is also written to `rack.errors`, so it lands in the server log even with no tracker installed.
|
|
491
|
+
|
|
492
|
+
**Opting out.** To keep the pre-4.0 behaviour and have the exception propagate out of the middleware stack:
|
|
493
|
+
|
|
494
|
+
```ruby
|
|
495
|
+
Grape.configure do |config|
|
|
496
|
+
config.raise_rendering_errors = true
|
|
497
|
+
end
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
With this on, the failsafe never runs: the exception is re-raised untouched, so neither env key is set and nothing is written to `rack.errors` — whatever caught it before catches it again.
|
|
501
|
+
|
|
502
|
+
Exceptions that no `rescue_from` matches still propagate exactly as before; only rendering failures changed.
|
|
503
|
+
|
|
504
|
+
#### `Array`/`Set` of an unsupported type is rejected when the API is defined
|
|
505
|
+
|
|
506
|
+
Declaring a collection whose element type Grape cannot coerce — `type: Array[Foo]` or `type: Set[Foo]` where `Foo` is neither a primitive, a structure, nor a valid custom type — now raises as soon as the `params` block is evaluated, i.e. while the API class is being loaded:
|
|
507
|
+
|
|
508
|
+
```
|
|
509
|
+
ArgumentError: type Foo should support coercion via `[]`
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
This is a **load-time** failure. An application that boots today can fail to boot on 4.0 without any request being made.
|
|
513
|
+
|
|
514
|
+
A valid custom type is one that implements a class-level `parse` taking exactly one argument (`Grape::Validations::Types.custom?`). A class that implements neither that nor `[]` was never coercible, so such a declaration was always a misconfiguration — but it used to surface much later, and much less clearly.
|
|
515
|
+
|
|
516
|
+
**What changed.** Nothing about which types are supported; only *when* the element coercer is built. [#2817](https://github.com/ruby-grape/grape/pull/2817) made `ArrayCoercer` build it eagerly in the constructor rather than memoizing it on first use, because coercers are shared across requests and must not create state at request time. Building it eagerly means the unsupported element type is discovered while the route is being defined.
|
|
517
|
+
|
|
518
|
+
Previously the coercer was built on the first request that actually supplied the parameter. An API that declared the parameter but was never sent one — a documentation-only declaration, for instance — never built it and never raised. When a request did supply it, the same `ArgumentError` was raised inside the coercer and swallowed by the coercion validator into a generic `400`:
|
|
519
|
+
|
|
520
|
+
```json
|
|
521
|
+
{ "error": "foo is invalid" }
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
so the misconfiguration presented as a puzzling per-request validation failure rather than as a broken declaration.
|
|
525
|
+
|
|
526
|
+
Note that the non-collection form has always raised at definition time:
|
|
527
|
+
|
|
528
|
+
| declaration | before | 4.0 |
|
|
529
|
+
| --- | --- | --- |
|
|
530
|
+
| `type: Foo` | `ArgumentError` when defined | unchanged |
|
|
531
|
+
| `type: Array[Foo]` | accepted; `400 "is invalid"` per request | `ArgumentError` when defined |
|
|
532
|
+
|
|
533
|
+
The collection form's leniency was an accident of the lazy build, not a supported behavior. The two are now consistent.
|
|
534
|
+
|
|
535
|
+
**Fixing a declaration that now raises.** Give the type a one-argument `parse`, which is what makes it a custom type:
|
|
536
|
+
|
|
537
|
+
```ruby
|
|
538
|
+
class Foo
|
|
539
|
+
def self.parse(value)
|
|
540
|
+
new(value)
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
params { requires :foos, type: Array[Foo] }
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Or coerce the collection yourself, in which case Grape does not build an element coercer at all:
|
|
548
|
+
|
|
549
|
+
```ruby
|
|
550
|
+
params { requires :foos, type: Array[Foo], coerce_with: ->(value) { Array(value).map { |v| Foo.new(v) } } }
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Or, if the class is only there to describe the parameter and never to coerce it — the common case behind this break — drop `type:` and document it instead:
|
|
554
|
+
|
|
555
|
+
```ruby
|
|
556
|
+
params { requires :foos, documentation: { type: Foo } }
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Defining `self.[]` on the class silences the load-time error, because that is the escape hatch for `dry-types` objects, but it does **not** make the type coercible: such a parameter still fails with `400 "is invalid"` on every request. Prefer `parse` or `coerce_with`.
|
|
560
|
+
|
|
561
|
+
#### The `cascade` getter returns the configured value
|
|
562
|
+
|
|
563
|
+
Calling `cascade` with no argument used to report whether cascading had been *configured at all* (`cascade false` still read back as `true`, contradicting the actual runtime behavior, which was correctly disabled). It now returns the configured value itself — `true`/`false` as set, or `true` when never set. Code that used the getter to detect "was `cascade` called" rather than "does this API cascade" must track that separately.
|
|
564
|
+
#### A cascading route hands over to every remaining route
|
|
565
|
+
|
|
566
|
+
A route that answers with `X-Cascade: pass` — what a version mismatch does by default — now hands the request to every remaining matching route, in registration order, before the router gives up. Previously it handed over to the *last* route registered for the path and stopped there, so with three or more routes sharing a path (typically three mounted API versions) every route in between was unreachable.
|
|
567
|
+
|
|
568
|
+
Two consequences:
|
|
569
|
+
|
|
570
|
+
* **A middle version is now served.** `mount v1; mount v2; mount v3` alongside a catch-all `route :any, '*path'` answered `406 API version not found` for v2, while v1 and v3 worked; v2 is now served.
|
|
571
|
+
* **An unmatched version reaches a catch-all.** When a catch-all ANY route is present, a request whose version matches nothing now falls through to it instead of surfacing the versioner's `406`, which is what `cascade: true` (the default) asks for. Declare `version ..., cascade: false` to keep the hard 406 — that path is unchanged and never consults the catch-all.
|
|
572
|
+
#### `Grape::Util::InheritableValues` has been removed
|
|
573
|
+
|
|
574
|
+
Inheritable settings no longer layer a scope's values over a store handed down from the enclosing scope. Each `Grape::Util::InheritableSetting` now keeps only what its own scope assigned, and resolves a lookup by walking `#parent` — the same move `Grape::Util::StackableValues` got in [#2823](https://github.com/ruby-grape/grape/pull/2823), and the reason `Grape::Util::InheritableValues` no longer has anything to do.
|
|
575
|
+
|
|
576
|
+
Resolution is unchanged in every observable way: the nearest scope wins, a scope may override an inherited value with an explicit `nil`, and a value an enclosing scope gains later is still visible through scopes already nested inside it. Only the class is gone. Nothing in Grape referenced it outside `InheritableSetting`; code that constructed one directly should keep a plain Hash per scope and resolve against the parent chain.
|
|
577
|
+
|
|
578
|
+
#### `forward_match` is no longer exposed on routes
|
|
579
|
+
|
|
580
|
+
`forward_match` is an internal, construction-time detail that decides how a route matches incoming paths (a prefix match for mounted Rack apps, the compiled pattern otherwise). It is now passed to `Grape::Router::Route` as a keyword argument and derived from the mounted app instead of being carried in the route's options.
|
|
581
|
+
|
|
582
|
+
As a result it is no longer readable through `route.options[:forward_match]` or the `route.forward_match` reader — both previously returned the flag. Nothing in Grape consumed either, so this only affects code that introspected routes directly; there is no replacement, as the value is now purely internal.
|
|
583
|
+
|
|
584
|
+
#### `Grape::Endpoint.new` takes `http_methods:` instead of `method:`
|
|
585
|
+
|
|
586
|
+
`Grape::Endpoint.new` now receives the HTTP verb(s) under the `http_methods:` keyword instead of `method:`, matching the name used everywhere else. If you build endpoints directly (uncommon — this is an internal API normally driven by the routing DSL), rename the keyword:
|
|
587
|
+
|
|
588
|
+
```ruby
|
|
589
|
+
# before
|
|
590
|
+
Grape::Endpoint.new(settings, method: :get, path: '/foo', for: self)
|
|
591
|
+
|
|
592
|
+
# after
|
|
593
|
+
Grape::Endpoint.new(settings, http_methods: :get, path: '/foo', for: self)
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
Relatedly, an endpoint's public `options` Hash no longer carries `:method` or `:path`. Nothing in Grape read `:method`, and the only reader of `:path` was an internal test; both values are available from the route instead — `route.request_method` for the verb and `route.path` for the (compiled) path, which is what grape-swagger and other introspection already use. The raw definition-time path array is no longer exposed on the endpoint.
|
|
597
|
+
|
|
598
|
+
#### `Grape::Endpoint.new` takes `api:` instead of `for:`
|
|
599
|
+
|
|
600
|
+
The keyword identifying the API an endpoint belongs to has been renamed from `for:` — a reserved word whose value can't be referenced as a local — to `api:`:
|
|
601
|
+
|
|
602
|
+
```ruby
|
|
603
|
+
# before
|
|
604
|
+
Grape::Endpoint.new(settings, http_methods: :get, path: '/foo', for: my_api)
|
|
605
|
+
|
|
606
|
+
# after
|
|
607
|
+
Grape::Endpoint.new(settings, http_methods: :get, path: '/foo', api: my_api)
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
The owning API is no longer carried on the endpoint's public `options` Hash — `endpoint.options[:for]` is gone. Use the new `endpoint.api` reader instead.
|
|
611
|
+
|
|
612
|
+
#### Route metadata is exposed through readers, not the `options` Hash
|
|
613
|
+
|
|
614
|
+
A route's computed metadata — `version`, `namespace`, `prefix`, `requirements`, `anchor` and `settings` — is now exposed through plain readers instead of being merged into the route's `options` Hash. `namespace`, `prefix` and `settings` are passed to `Grape::Router::Route` as explicit keyword arguments; `version`, `anchor` and `requirements` are read from the route's pattern (they shape how it matches).
|
|
615
|
+
|
|
616
|
+
The readers are unchanged — keep using them:
|
|
617
|
+
|
|
618
|
+
```ruby
|
|
619
|
+
route.version # => 'v1'
|
|
620
|
+
route.namespace # => '/things'
|
|
621
|
+
route.prefix # => 'api'
|
|
622
|
+
route.requirements # => {}
|
|
623
|
+
route.anchor # => true
|
|
624
|
+
route.settings # => { ... }
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
What changed is the raw bag. `route.options` now holds only what was declared for the route, so it no longer carries the *computed* values for these keys — `route.options[:version]`, `[:namespace]`, `[:prefix]` and `[:settings]` return `nil`. Nothing in Grape or grape-swagger read them that way (grape-swagger uses the `route.prefix` and `route.settings` readers). `requirements` and `anchor` — which can be supplied as route options (e.g. a mount's `anchor: false`) — are covered separately below: they too are now first-class endpoint inputs and likewise no longer appear in `route.options`. The effective value always comes from the reader.
|
|
628
|
+
|
|
629
|
+
#### `Grape::Endpoint` no longer accepts a `format:` keyword
|
|
630
|
+
|
|
631
|
+
The `:format` member was removed from the endpoint's internal `Options`. Nothing ever passed `format:` to `Grape::Endpoint.new` — `config.format` was always `nil` — so this has no runtime effect (the `(.:format)`/`(.json)` route suffix comes from `Grape::Path`, not from this value). But `Grape::Endpoint.new(..., format: …)` now raises `unknown keyword: :format` instead of silently ignoring it.
|
|
632
|
+
|
|
633
|
+
`Grape::Router::Pattern#initialize` no longer accepts `format:` either. It was never given a non-`nil` value — a route's `:format` capture comes from the path suffix built by `Grape::Path`, not from the pattern — so the keyword was dead. `Grape::Router::Pattern.new(..., format: …)` now raises `unknown keyword: :format`.
|
|
634
|
+
|
|
635
|
+
#### `desc` no longer populates `namespace_setting(:description)`
|
|
636
|
+
|
|
637
|
+
`desc` used to store its settings under both the route scope (`route_setting(:description)`) and the namespace scope (`namespace_setting(:description)`). The namespace copy was write-only — the namespace scope isn't wired for inheritance and nothing in Grape or grape-swagger ever read it — so `desc` now writes only the route scope. `namespace_setting(:description)` returns `nil`; read a route's description through `route_setting(:description)` or the `route.description` reader.
|
|
638
|
+
#### `Grape::Router::Route#params` no longer takes an argument
|
|
639
|
+
|
|
640
|
+
`Route#params` used to do two jobs depending on its argument: `route.params(input)` extracted param values from a matched request path, while `route.params` (no argument) returned the route's declared param definitions. These are now separate methods:
|
|
641
|
+
|
|
642
|
+
```ruby
|
|
643
|
+
route.params # declared param definitions, keyed by name (unchanged)
|
|
644
|
+
route.params_for(input) # values extracted from a matched path (was route.params(input))
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
The no-argument form is unchanged — grape-swagger and other documentation consumers keep using `route.params`. Only the value-extraction form moved, and it is internal to the router; if you called `route.params(input)` directly, switch to `route.params_for(input)`.
|
|
648
|
+
|
|
649
|
+
#### `params` is a first-class endpoint input, no longer in `route.options`
|
|
650
|
+
|
|
651
|
+
A route's declared params were previously carried inside the `route_options` bag and reachable as `route.options[:params]`. They are now composed into their own endpoint input (`Grape::Endpoint::Options` gains a `:params` member and `Grape::Endpoint.new` a `params:` keyword) and exposed only through `route.params`. `route.options[:params]` now returns `nil`. Nothing in Grape or grape-swagger read it that way — grape-swagger uses the `route.params` method — so this only affects code that reached into the options Hash for params directly.
|
|
652
|
+
|
|
653
|
+
#### `requirements` and `anchor` are first-class endpoint inputs, no longer in `route.options`
|
|
654
|
+
|
|
655
|
+
Like `params` above, a route's `requirements` and `anchor` were previously carried inside the `route_options` bag. They are now composed into their own endpoint inputs (`Grape::Endpoint::Options` gains `:requirements` and `:anchor` members, and `Grape::Endpoint.new` gains `requirements:` and `anchor:` keywords) and exposed only through the `route.requirements` and `route.anchor` readers. `route.options[:requirements]` and `route.options[:anchor]` now return `nil` — including for a mount's `anchor: false`. Nothing in Grape or grape-swagger read them that way, so this only affects code that reached into the options Hash for these keys directly.
|
|
656
|
+
|
|
657
|
+
#### Params state is recorded through `InheritableSetting` accessors
|
|
658
|
+
|
|
659
|
+
The state accumulated by `params` / `contract` blocks — validator instances, declared params, param documentation and reusable named params — is now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`validations` / `add_validation`, `declared_params` / `add_declared_params`, `params_documentation` / `add_params_documentation`, `named_params` / `add_named_params`, `reset_validations!`) instead of raw `namespace_stackable` keys, following the same move made for rescue handlers. The keys' storage is unchanged for now, so `namespace_stackable[:validations]` and friends still return the same values, but they should be considered internal.
|
|
660
|
+
|
|
661
|
+
Related contract changes, none with known external consumers:
|
|
662
|
+
|
|
663
|
+
* `Grape::Endpoint#inherit_settings` now takes the parent `Grape::Util::InheritableSetting` instead of that setting's raw `namespace_stackable` store.
|
|
664
|
+
* The endpoint's request-time validator snapshot moved from `route_setting(:saved_validations)` to `route_setting(:validations)`, symmetric with `route_setting(:declared_params)`. The vestigial `saved_` prefix dated back to the 2014 settings refactor. Neither key appears in `route.settings`, as before.
|
|
665
|
+
* `Grape::Util::InheritableSetting#api_class` (a Hash nothing in Grape ever wrote to) and `#point_in_time_copies` (only ever used internally) are removed.
|
|
666
|
+
#### Callback filters are recorded through `InheritableSetting` accessors
|
|
667
|
+
|
|
668
|
+
The filter blocks registered by `before`, `before_validation`, `after_validation`, `after` and `finally` are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` — `add_callback(name, block)` to record, and `callbacks` returning a Hash keyed by the DSL method names (`callbacks[:before]`, `callbacks[:finally]`, …) — instead of raw `namespace_stackable` keys, following the same move made for rescue handlers. The keys' storage is unchanged for now, so `namespace_stackable[:befores]` and friends still return the same values, but the pluralized keys should be considered internal.
|
|
669
|
+
#### Rescue configuration is recorded through `InheritableSetting` accessors
|
|
670
|
+
|
|
671
|
+
The remaining rescue state written by `rescue_from` is now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting`, completing the encapsulation started with `rescue_handlers` / `add_rescue_handlers`:
|
|
672
|
+
|
|
673
|
+
* `rescue_options` / `add_rescue_options(options)` replace `namespace_stackable[:rescue_options]`. The reader absorbs the nearest-scope-wins convention (previously the `&.last` at the read site) and returns a single `Grape::DSL::RescueOptions` or `nil`, not the raw stack.
|
|
674
|
+
* `add_all_rescue_handler(handler)`, `add_grape_exceptions_rescue_handler(handler)` and `add_internal_grape_exceptions_rescue_handler(handler)` replace the direct `namespace_inheritable` writes for the `rescue_from :all` / `:grape_exceptions` / `:internal_grape_exceptions` meta selectors; each records its handler and flips the associated flags.
|
|
675
|
+
* `rescue_all?`, `rescue_grape_exceptions?`, `all_rescue_handler`, `grape_exceptions_rescue_handler` and `internal_grape_exceptions_rescue_handler` replace the corresponding `namespace_inheritable` reads. The two `?` readers return `false` (rather than `nil`) when never set; `Grape::Middleware::Error` only ever used them in boolean context, so behavior is unchanged.
|
|
676
|
+
|
|
677
|
+
The keys' storage is unchanged for now, so `namespace_stackable[:rescue_options]` and the `namespace_inheritable` keys still return the same values, but they should be considered internal.
|
|
678
|
+
#### Content negotiation state is recorded through `InheritableSetting` accessors
|
|
679
|
+
|
|
680
|
+
The state written by the `content_type`, `format`, `formatter`, `parser` and `error_formatter` DSL methods is now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`content_types` / `add_content_type`, `formatters` / `add_formatter`, `parsers` / `add_parser`, `error_formatters` / `add_error_formatter`) instead of raw `namespace_stackable` keys, following the same move made for rescue handlers. Each writer records one single-entry Hash per registration and each reader absorbs the `namespace_stackable_with_hash` deep-merge convention, returning the merged name => value Hash (nearest scope wins) or `nil` when nothing is registered. The keys' storage is unchanged for now, so `namespace_stackable[:content_types]` and friends still return the same values, but they should be considered internal.
|
|
681
|
+
#### `represent` registrations are recorded through `InheritableSetting` accessors
|
|
682
|
+
|
|
683
|
+
The model-class => entity-class registrations written by `represent` are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`representations` / `add_representation(model_class, entity_class)`) instead of raw `namespace_stackable` keys, following the same move made for rescue handlers. The reader absorbs the `namespace_stackable_with_hash` deep-merge convention, returning the merged Hash (nearest scope wins) or `nil` when nothing is registered. The key's storage is unchanged for now, so `namespace_stackable[:representations]` still returns the same values, but it should be considered internal.
|
|
684
|
+
|
|
685
|
+
Also removed: the `namespace_stackable[:representations] ||= []` line in `Grape::Endpoint#initialize`. It was provably inert — `Grape::Util::StackableValues#[]` always returns an array (a frozen empty one for never-written keys), which is truthy, so the `||=` assignment could never execute. No settings state changes as a result.
|
|
686
|
+
#### Middleware and helper registrations are recorded through `InheritableSetting` accessors
|
|
687
|
+
|
|
688
|
+
The state written by the middleware DSL (`use`, `insert`, `insert_before`, `insert_after`) and by `helpers` is now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`middleware` / `add_middleware`, `helpers` / `add_helper`) instead of raw `namespace_stackable` keys, following the same move made for rescue handlers. The keys' storage is unchanged for now, so `namespace_stackable[:middleware]` and `namespace_stackable[:helpers]` still return the same values, but they should be considered internal.
|
|
689
|
+
|
|
690
|
+
One micro-change: the `middleware` DSL reader dropped its `|| []` fallback — `Grape::Util::StackableValues#[]` never returns `nil`, so the fallback was dead code. When no middleware is registered the reader now returns the shared frozen empty array instead of a fresh mutable one; no caller in Grape or grape-swagger mutates the returned array.
|
|
691
|
+
#### Namespace and mount-path registrations are recorded through `InheritableSetting` accessors
|
|
692
|
+
|
|
693
|
+
The `Grape::Namespace` objects registered by the `namespace` DSL (and its `group` / `resource` / `resources` / `segment` aliases) and the mount path recorded by `mount` are now written and read through dedicated accessors on `Grape::Util::InheritableSetting` instead of raw `namespace_stackable` keys, following the same move made for rescue handlers:
|
|
694
|
+
|
|
695
|
+
* `namespaces` / `add_namespace(namespace)` replace `namespace_stackable[:namespace]` (not to be confused with the pre-existing `namespace` reader, which returns the namespace-settings store).
|
|
696
|
+
* `namespace_path` returns the normalized joined path prefix, absorbing the `Grape::Namespace.joined_space_path(...)` call previously spelled out at the read sites, and `namespace_requirements` returns the requirements declared by registered namespaces, absorbing the `filter_map(&:requirements)`.
|
|
697
|
+
* `mount_path` / `add_mount_path(path)` replace `namespace_stackable[:mount_path]`; the reader absorbs the outermost-wins convention (previously the `.first` at the read site in `Endpoint#build_stack`).
|
|
698
|
+
|
|
699
|
+
The keys' storage is unchanged for now, so `namespace_stackable[:namespace]` and `namespace_stackable[:mount_path]` still return the same values, but they should be considered internal.
|
|
700
|
+
#### Contract key maps are recorded through `InheritableSetting` accessors
|
|
701
|
+
|
|
702
|
+
The Dry::Schema key maps registered by `contract` blocks are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`contract_key_maps` / `add_contract_key_map(key_map)`) instead of the raw `namespace_stackable[:contract_key_map]` key, following the same move made for rescue handlers. The key's storage is unchanged for now, so `namespace_stackable[:contract_key_map]` still returns the same values, but it should be considered internal.
|
|
703
|
+
#### Format and error-response defaults are recorded through `InheritableSetting` accessors
|
|
704
|
+
|
|
705
|
+
The nearest-wins scalars written by the `format`, `default_format`, `default_error_formatter` and `default_error_status` DSL methods are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`format` / `format=`, `default_format` / `default_format=`, `default_error_formatter` / `default_error_formatter=`, `default_error_status` / `default_error_status=`) instead of raw `namespace_inheritable` keys — the first batch of the `namespace_inheritable` cleanup, following the pattern used for `namespace_stackable`. Since these are overriding assignments rather than stacking registrations, the writers use plain `=` instead of the `add_*` naming. The keys' storage is unchanged for now, so `namespace_inheritable[:format]` and friends still return the same values, but they should be considered internal.
|
|
706
|
+
#### Versioning state is recorded through `InheritableSetting` accessors
|
|
707
|
+
|
|
708
|
+
The nearest-wins scalars written by the `version`, `prefix` and `cascade` DSL methods are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`version` / `version=`, `version_options` / `version_options=`, `root_prefix` / `root_prefix=`, `cascade` / `cascade=`) instead of raw `namespace_inheritable` keys. `cascade` is presence-sensitive — an explicit `nil` is distinct from never-set — so the accessor family includes `cascade_defined?`, absorbing the `namespace_inheritable.key?(:cascade)` checks previously spelled out in `DSL::Routing#cascade` and `Grape::API::Instance#cascade?`. The keys' storage is unchanged for now, so `namespace_inheritable[:version]` and friends still return the same values, but they should be considered internal.
|
|
709
|
+
#### Routing scope flags are recorded through `InheritableSetting` accessors
|
|
710
|
+
|
|
711
|
+
The flags flipped by `do_not_route_head!`, `do_not_route_options!`, `do_not_document!` and `lint!` are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`!` writers, `?` readers) instead of raw `namespace_inheritable` keys. The `?` readers return `false` (rather than `nil`) when never set; every consumer only used them in boolean context, so behavior is unchanged. The keys' storage is unchanged for now, so `namespace_inheritable[:do_not_route_head]` and friends still return the same values, but they should be considered internal.
|
|
712
|
+
#### `build_params_with` and `auth` are recorded through `InheritableSetting` accessors
|
|
713
|
+
|
|
714
|
+
The params-builder strategy written by `build_with` (both the API-level and the params-block DSL) and the authentication configuration written by the `auth` DSL are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`build_params_with` / `build_params_with=`, `auth` / `auth=`) instead of raw `namespace_inheritable` keys, completing the per-key `namespace_inheritable` cleanup. The keys' storage is unchanged for now, so `namespace_inheritable[:build_params_with]` and `namespace_inheritable[:auth]` still return the same values, but they should be considered internal.
|
|
715
|
+
|
|
716
|
+
#### `InheritableSetting`'s raw stores are internal, except `namespace_stackable`
|
|
717
|
+
|
|
718
|
+
The per-key encapsulation documented in the sections above is now enforced where the ecosystem allows: `Grape::Util::InheritableSetting#namespace_inheritable` is protected and `#namespace_stackable_with_hash` is private — every `namespace_inheritable` key is reachable only through its dedicated accessor.
|
|
719
|
+
|
|
720
|
+
`#namespace_stackable` deliberately **remains public**: grape-swagger reads it directly (`swagger_documentation_adder.rb` reads `[:namespace]` and `[:mount_path]`, `token_owner_resolver.rb` reads `[:helpers]`, and `request_param_parsers/route.rb` walks the `StackableValues#inherited_values` chain to collect per-scope params). Treat it as read-only from outside Grape and prefer the dedicated accessors (`namespaces`, `mount_paths`, `helpers`, …) for everything they cover; the reader may be narrowed in a future major once grape-swagger has migrated. The `route`, `namespace` and `global` scratch stores (backing `route_setting` / `namespace_setting` / `global_setting`) also remain public and unchanged.
|
|
721
|
+
|
|
722
|
+
Two supporting changes:
|
|
723
|
+
|
|
724
|
+
* `Router::Pattern::Path` no longer receives a merged dump of both stores; `Endpoint#to_routes` passes the new `InheritableSetting#path_settings` snapshot — a `Grape::Util::InheritableSetting::PathSettings` value object (`Data`) — which carries exactly the six values `Path` reads (full mount-path stack, root prefix, format, raw content-types stack, version, version options) with always-present keys. `Path`'s `key?`-presence guards became nil-tolerant truthiness checks — equivalent under the snapshot. `Endpoint#prepare_default_path_settings` is removed.
|
|
725
|
+
* `InheritableSetting#mount_paths` is added, exposing the full mount-path stack (one entry per mount level, outermost first), complementing `#mount_path`, which returns only the outermost entry.
|
|
726
|
+
|
|
727
|
+
#### `Grape::Util::StackableValues` is a read-only view, not a store
|
|
728
|
+
|
|
729
|
+
Stackable registrations now live on `Grape::Util::InheritableSetting` itself, as one plain Hash of `key => Array` per scope, and inheritance is resolved by walking `#parent` — the same recursion `#rescue_handlers` already used. Previously the registrations lived in a `StackableValues` instance which maintained its own parallel chain through `#inherited_values`, so every scope carried two links to its parent.
|
|
730
|
+
|
|
731
|
+
`InheritableSetting#namespace_stackable` still returns a `Grape::Util::StackableValues`, still resolves reads across the chain, and still exposes `#new_values` / `#inherited_values` / `#keys` / `#to_hash`, so the grape-swagger call sites listed above keep working unchanged. What changed:
|
|
732
|
+
|
|
733
|
+
* It is now a **view built on demand**, not the backing store. Each call returns a new instance, so the object is no longer identity-stable across calls, and writing to it registers nothing.
|
|
734
|
+
* `StackableValues#[]=` and `#delete` are removed — the class is read-only. Registration happens through the semantic `add_*` accessors on `InheritableSetting`.
|
|
735
|
+
* `StackableValues.new` now takes `(new_values, inherited_values)` instead of `(inherited_values = nil)`.
|
|
736
|
+
* `Grape::Util::BaseInheritable` is removed. It existed only to share plumbing between `StackableValues` and `InheritableValues`; with the former no longer a store, its contents moved into `Grape::Util::InheritableValues`, whose public behavior is unchanged (its now-unused `#keys` is dropped).
|
|
737
|
+
|
|
738
|
+
Settings semantics are otherwise deliberately identical, including two pre-existing quirks worth knowing about, since neither is fixed here:
|
|
739
|
+
|
|
740
|
+
* A stack reader returns the backing Array itself when only the current scope registered anything (a frozen empty Array when nothing is registered anywhere), so callers must treat the result as read-only.
|
|
741
|
+
* `#point_in_time_copy` copies the per-scope Hash shallowly, so the per-key Arrays stay shared with the source. A registration made on a scope *after* an endpoint was defined in it is therefore still visible to that endpoint — but only when the same key already had a registration when the endpoint was defined. This is why `use SomeMiddleware` written below a route can still apply to it.
|
|
742
|
+
|
|
4
743
|
### Upgrading to >= 3.3
|
|
5
744
|
|
|
6
745
|
#### Minimum required Ruby is now 3.3
|
|
@@ -27,6 +766,8 @@ Grape::Exceptions::ValidationErrors.new(errors: [validation, validation_array_er
|
|
|
27
766
|
|
|
28
767
|
# after
|
|
29
768
|
Grape::Exceptions::ValidationErrors.new(exceptions: [validation, validation_array_errors], headers:)
|
|
769
|
+
```
|
|
770
|
+
|
|
30
771
|
#### `Grape::Exceptions::ValidationErrors` no longer mixes in `Enumerable`
|
|
31
772
|
|
|
32
773
|
`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.
|
|
@@ -63,16 +804,16 @@ rescue_from MyError, with: :other_handler
|
|
|
63
804
|
|
|
64
805
|
Calls that only use one meta selector or only use exception classes (the documented forms) are unaffected.
|
|
65
806
|
|
|
66
|
-
#### `auth
|
|
807
|
+
#### `auth` and `http_basic` now take keyword arguments
|
|
67
808
|
|
|
68
|
-
`Grape::Middleware::Auth::DSL#auth
|
|
809
|
+
`Grape::Middleware::Auth::DSL#auth` and `#http_basic` now accept their options as keyword arguments instead of a positional `Hash`. Calls using bare keyword syntax or a block are unaffected:
|
|
69
810
|
|
|
70
811
|
```ruby
|
|
71
812
|
http_basic realm: 'API' do |u, p|
|
|
72
813
|
# ...
|
|
73
814
|
end
|
|
74
815
|
|
|
75
|
-
auth :
|
|
816
|
+
auth :my_strategy, realm: 'API', &proc
|
|
76
817
|
```
|
|
77
818
|
|
|
78
819
|
Passing a positional options `Hash` still works but is deprecated and will be removed in a future release:
|
|
@@ -80,11 +821,11 @@ Passing a positional options `Hash` still works but is deprecated and will be re
|
|
|
80
821
|
```ruby
|
|
81
822
|
# deprecated
|
|
82
823
|
http_basic({ realm: 'API' })
|
|
83
|
-
auth :
|
|
824
|
+
auth :my_strategy, { realm: 'API' }
|
|
84
825
|
|
|
85
826
|
# preferred
|
|
86
827
|
http_basic(realm: 'API')
|
|
87
|
-
auth :
|
|
828
|
+
auth :my_strategy, realm: 'API'
|
|
88
829
|
```
|
|
89
830
|
|
|
90
831
|
#### Middleware options now route through per-class `Options` `Data` value objects
|
|
@@ -466,7 +1207,16 @@ Grape has been modernized to use Ruby 3+'s preferred argument delegation pattern
|
|
|
466
1207
|
- Method signatures are now more explicit and follow Ruby 3+ best practices
|
|
467
1208
|
- The `active_support/core_ext/array/extract_options` dependency has been removed
|
|
468
1209
|
|
|
469
|
-
|
|
1210
|
+
Passing the options of `requires`, `optional` and `use` as a trailing positional Hash still works, but is deprecated:
|
|
1211
|
+
|
|
1212
|
+
```ruby
|
|
1213
|
+
params do
|
|
1214
|
+
requires :id, { type: Integer } # deprecated
|
|
1215
|
+
requires :id, type: Integer # do this instead
|
|
1216
|
+
end
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
Under `extract_options!` the braces made no difference. They do now: Ruby only turns a trailing Hash into keyword arguments when it is written without braces, so a braced Hash lands in the splat and would otherwise be taken for a parameter name.
|
|
470
1220
|
|
|
471
1221
|
See [#2618](https://github.com/ruby-grape/grape/pull/2618) for more information.
|
|
472
1222
|
|