grape 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +108 -1
- data/README.md +195 -95
- data/UPGRADING.md +364 -0
- data/grape.gemspec +1 -1
- data/lib/grape/api/instance.rb +57 -30
- data/lib/grape/api.rb +52 -18
- data/lib/grape/content_types.rb +26 -9
- data/lib/grape/cookies.rb +1 -1
- data/lib/grape/declared_params_handler.rb +35 -7
- data/lib/grape/dsl/declared.rb +5 -4
- data/lib/grape/dsl/desc.rb +1 -2
- data/lib/grape/dsl/entity.rb +35 -19
- data/lib/grape/dsl/headers.rb +3 -3
- data/lib/grape/dsl/helpers.rb +5 -8
- data/lib/grape/dsl/inside_route.rb +33 -27
- data/lib/grape/dsl/logger.rb +10 -4
- data/lib/grape/dsl/middleware.rb +4 -9
- data/lib/grape/dsl/mounting.rb +149 -0
- data/lib/grape/dsl/parameters.rb +78 -104
- data/lib/grape/dsl/request_response.rb +35 -22
- data/lib/grape/dsl/routing.rb +17 -191
- data/lib/grape/dsl/settings.rb +1 -1
- data/lib/grape/dsl/version_options.rb +2 -2
- data/lib/grape/dsl/versioning.rb +98 -0
- data/lib/grape/endpoint/state.rb +15 -0
- data/lib/grape/endpoint.rb +229 -137
- data/lib/grape/error_formatter/base.rb +28 -13
- data/lib/grape/error_formatter/json.rb +3 -0
- data/lib/grape/error_formatter/jsonl.rb +15 -0
- data/lib/grape/error_formatter/xml.rb +3 -1
- data/lib/grape/exceptions/error_response.rb +23 -7
- data/lib/grape/exceptions/halt.rb +39 -0
- data/lib/grape/exceptions/validation.rb +9 -9
- data/lib/grape/exceptions/validation_errors.rb +12 -2
- data/lib/grape/formatter/jsonl.rb +34 -0
- data/lib/grape/formatter/markdown.rb +17 -0
- data/lib/grape/formatter/md.rb +11 -0
- data/lib/grape/locale/en.yml +1 -0
- data/lib/grape/middleware/auth/base.rb +16 -1
- data/lib/grape/middleware/auth/strategy_info.rb +2 -2
- data/lib/grape/middleware/base.rb +19 -13
- data/lib/grape/middleware/error.rb +133 -35
- data/lib/grape/middleware/formatter.rb +235 -94
- data/lib/grape/middleware/precomputed_content_types.rb +14 -2
- data/lib/grape/middleware/stack.rb +7 -8
- data/lib/grape/middleware/versioner/base.rb +26 -10
- data/lib/grape/middleware/versioner/header.rb +70 -15
- data/lib/grape/middleware/versioner/param.rb +3 -5
- data/lib/grape/middleware/versioner/path.rb +20 -11
- data/lib/grape/middleware/versioner.rb +1 -1
- data/lib/grape/mountable.rb +2 -4
- data/lib/grape/namespace.rb +2 -3
- data/lib/grape/parser/jsonl.rb +19 -0
- data/lib/grape/request.rb +46 -8
- data/lib/grape/router/base_route.rb +41 -0
- data/lib/grape/router/greedy_route.rb +1 -1
- data/lib/grape/router/mustermann_pattern.rb +1 -1
- data/lib/grape/router/pattern/path.rb +1 -1
- data/lib/grape/router/pattern.rb +11 -5
- data/lib/grape/router/route.rb +42 -11
- data/lib/grape/router/route_buckets.rb +246 -0
- data/lib/grape/router.rb +262 -32
- data/lib/grape/util/endpoint_configuration.rb +7 -1
- data/lib/grape/util/inheritable_setting.rb +80 -161
- data/lib/grape/util/lazy/value.rb +52 -11
- data/lib/grape/util/media_type.rb +38 -15
- data/lib/grape/util/path_normalizer.rb +7 -2
- data/lib/grape/util/registry.rb +9 -2
- data/lib/grape/util/scrub.rb +33 -0
- data/lib/grape/util/translation.rb +6 -6
- data/lib/grape/validations/attributes_iterator.rb +73 -37
- data/lib/grape/validations/coerce_options.rb +1 -1
- data/lib/grape/validations/params_documentation.rb +15 -7
- data/lib/grape/validations/params_scope.rb +176 -43
- data/lib/grape/validations/types/custom_type_coercer.rb +2 -3
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +20 -12
- data/lib/grape/validations/types/multiple_type_coercer.rb +1 -2
- data/lib/grape/validations/types/primitive_coercer.rb +17 -0
- data/lib/grape/validations/types/variant_collection_coercer.rb +24 -10
- data/lib/grape/validations/types.rb +12 -18
- data/lib/grape/validations/validations_spec.rb +53 -22
- data/lib/grape/validations/validators/allow_blank_validator.rb +2 -2
- data/lib/grape/validations/validators/as_validator.rb +2 -3
- data/lib/grape/validations/validators/base.rb +124 -25
- data/lib/grape/validations/validators/coerce_validator.rb +2 -3
- data/lib/grape/validations/validators/default_validator.rb +18 -2
- data/lib/grape/validations/validators/except_values_validator.rb +16 -3
- data/lib/grape/validations/validators/hash_elements_validator.rb +34 -0
- data/lib/grape/validations/validators/length_validator.rb +24 -16
- data/lib/grape/validations/validators/multiple_params_base.rb +0 -4
- data/lib/grape/validations/validators/values_validator.rb +18 -24
- data/lib/grape/version.rb +1 -2
- data/lib/grape.rb +3 -0
- metadata +18 -11
- data/lib/grape/util/lazy/value_array.rb +0 -21
- data/lib/grape/util/lazy/value_enumerable.rb +0 -31
- data/lib/grape/util/lazy/value_hash.rb +0 -21
- data/lib/grape/validations/multiple_attributes_iterator.rb +0 -13
- data/lib/grape/validations/single_attribute_iterator.rb +0 -28
data/UPGRADING.md
CHANGED
|
@@ -1,6 +1,370 @@
|
|
|
1
1
|
Upgrading Grape
|
|
2
2
|
===============
|
|
3
3
|
|
|
4
|
+
### Upgrading to >= 4.1.0
|
|
5
|
+
|
|
6
|
+
#### `Rack::Head` wraps the router instead of each endpoint's stack
|
|
7
|
+
|
|
8
|
+
`Rack::Head` used to be the first middleware in each endpoint's stack. That missed a Rack app mounted with `mount`, which the router calls directly, and a middleware inserted ahead of it with `insert 0`: a `HEAD` request to either got the full body back. A single `Rack::Head` around the router now strips the body of every `HEAD` response the API gives ([#3042](https://github.com/ruby-grape/grape/pull/3042)).
|
|
9
|
+
|
|
10
|
+
An endpoint's stack now starts with `Grape::Middleware::Error`, and three things follow from that:
|
|
11
|
+
|
|
12
|
+
* `insert_before Rack::Head` and `insert_after Rack::Head` raise `No such middleware to insert before: Rack::Head` (or `after`) when the API compiles. Use `insert 0` to put a middleware first.
|
|
13
|
+
* A middleware inserted by index lands one place further in. `insert 1` used to put it between `Rack::Head` and `Grape::Middleware::Error`; it now puts it inside `Grape::Middleware::Error`, so `rescue_from` handles what it raises. Use `insert 0` to keep it outside.
|
|
14
|
+
* A middleware inserted with `insert 0` now sees the full body of a `HEAD` response, as one added with `use` always has.
|
|
15
|
+
|
|
16
|
+
#### A `:jsonl` format you declared now gets Grape's JSON Lines formatter, parser and error formatter
|
|
17
|
+
|
|
18
|
+
Grape now renders, parses and reports errors in JSON Lines under `:jsonl` ([#3029](https://github.com/ruby-grape/grape/pull/3029)). An API that declared `content_type :jsonl, ...` itself, with nothing of its own registered for it, gets Grape's:
|
|
19
|
+
|
|
20
|
+
* A response body that is not a String renders as JSON Lines. A String is still served as it is.
|
|
21
|
+
* A request body is parsed into an Array in `env['api.request.body']`, where it used to be the raw String. Keep the String with `parser :jsonl, nil`.
|
|
22
|
+
* An error renders as a JSON line instead of through `default_error_formatter`.
|
|
23
|
+
|
|
24
|
+
A formatter, parser or error formatter you register for `:jsonl` with `formatter`, `parser` or `error_formatter` still takes precedence.
|
|
25
|
+
|
|
26
|
+
#### Formats named `:md` and `:markdown` have a built-in formatter
|
|
27
|
+
|
|
28
|
+
`Grape::Formatter::Md` renders the `:md` format and `Grape::Formatter::Markdown` the `:markdown` one ([#3025](https://github.com/ruby-grape/grape/pull/3025)). Neither is a default content type, so only an API that declares one is affected. See [Serving Markdown](README.md#serving-markdown).
|
|
29
|
+
|
|
30
|
+
* An API that declared `content_type :md` (or `:markdown`) without a matching `formatter` used to send its bodies unchanged. A String still is. An object answering `to_markdown` is now rendered with it, and any other object raises `Grape::Exceptions::InvalidFormatter`: it answers in the default format when the client negotiated Markdown, and `500` otherwise.
|
|
31
|
+
* `:md` and `:markdown` now name the same format. An API that declared one of them used to answer the other extension (and `?format=`) in its default format, or `406`; it now answers it in Markdown, under the name it declared.
|
|
32
|
+
* A `formatter :md` or `formatter :markdown` of your own still takes precedence.
|
|
33
|
+
* A formatter class of your own named `Md` or `Markdown` that inherits `Grape::Formatter::Base` now replaces Grape's globally, with a warning that `md` or `markdown` is already registered. Rename it, and declare it with `formatter :md, YourFormatter` (or `:markdown`) in the APIs that use it.
|
|
34
|
+
|
|
35
|
+
#### `logger` applies to the API it is set on
|
|
36
|
+
|
|
37
|
+
`logger` kept the logger in settings shared by the whole process, so setting it on one API replaced it on every other: two APIs that each set a logger both logged to whichever was set last.
|
|
38
|
+
|
|
39
|
+
A logger now belongs to the API it is set on, and to the APIs mounted under it that set none of their own ([#3024](https://github.com/ruby-grape/grape/pull/3024)). An API that sets none, and is mounted under none that does, still logs to a default shared by every such API, so a level set on that default reaches all of them as before.
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
class Users < Grape::API
|
|
43
|
+
logger Logger.new('users.log')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class Orders < Grape::API
|
|
47
|
+
logger Logger.new('orders.log')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Users.logger Before: the orders.log logger After: the users.log logger
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If an API logged to a logger set on another API that does not mount it, set the logger on that API as well, or on an API that mounts both. Setting one on `Grape::API::Instance` no longer reaches the APIs you define.
|
|
54
|
+
|
|
55
|
+
#### `error!` raises `Grape::Exceptions::Halt` instead of throwing `:error`
|
|
56
|
+
|
|
57
|
+
`error!` used to `throw :error`, and a throw is not an exception. Active Record commits a transaction that a throw leaves, so `error!` inside `transaction` saved what the block had written while the client was told the request failed ([#2197](https://github.com/ruby-grape/grape/issues/2197)). A throw cannot leave the fiber or thread it was thrown in either, so `error!` inside an `Async` task answered 500 instead of its status.
|
|
58
|
+
|
|
59
|
+
`error!` now raises `Grape::Exceptions::Halt` ([#3023](https://github.com/ruby-grape/grape/pull/3023)), a `StandardError` carrying the same response (`#response`, also `#status` and `#headers`). Grape renders it as it rendered the thrown error, without handing it to a `rescue_from` handler, `rescue_from :all` included.
|
|
60
|
+
|
|
61
|
+
* A `rescue => e` or `rescue StandardError` around `error!` now catches it, whether in a route, a helper, a filter or a middleware added with `use`. Narrow the rescue, or let `Halt` through:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
rescue Grape::Exceptions::Halt
|
|
65
|
+
raise
|
|
66
|
+
rescue StandardError => e
|
|
67
|
+
# ...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
* Code that wraps `error!` in `catch(:error)`, a spec for instance, now gets the exception. Rescue `Grape::Exceptions::Halt` and read `#response`, the `Grape::Exceptions::ErrorResponse` that used to be thrown.
|
|
71
|
+
* An `ActiveSupport::Notifications` subscriber to `endpoint_run.grape`, `endpoint_render.grape` or `endpoint_run_filters.grape` now finds `:exception` and `:exception_object` in the payload when `error!` ends the request, as it does for any exception.
|
|
72
|
+
|
|
73
|
+
`throw :error` from your own middleware or `rescue_from` handlers still works.
|
|
74
|
+
|
|
75
|
+
#### `Grape::Exceptions::ErrorResponse` is no longer a `Data`
|
|
76
|
+
|
|
77
|
+
Every error response builds at least two `ErrorResponse`s, and building a `Data` costs several times what building a plain object does. It is now a plain frozen class ([#3023](https://github.com/ruby-grape/grape/pull/3023)). It is still built with keywords, each optional, and still answers its readers.
|
|
78
|
+
|
|
79
|
+
It no longer copies itself with `#with`, compares by value (`==`, `eql?`, `hash`), converts with `#to_h`, lists `members`, takes positional arguments or takes part in pattern matching. Build a new one with `ErrorResponse.new` instead of calling `#with`. To check a payload, read its fields, for instance with `have_attributes(status: 404, message: 'Not Found')` in a spec.
|
|
80
|
+
|
|
81
|
+
#### `Grape::Middleware::Versioner::Header` answers a common Accept header without `#before`
|
|
82
|
+
|
|
83
|
+
`Middleware::Base#call` copied the header versioner for every request so that `#before` could keep the env and the Accept header it scrubs in instance variables. When the API is not strict and the Accept header is one the versioner worked out when it was built (a media type it declares, `*/*` or none), it now records the media type and calls the app from the one instance the stack built ([#3017](https://github.com/ruby-grape/grape/pull/3017)). Any other request still goes through `#before`.
|
|
84
|
+
|
|
85
|
+
A subclass that overrides `#before` is therefore not called for those requests. Override `#call` instead, and keep what a request needs in locals or in the env rather than in instance variables: the instance is shared by every request.
|
|
86
|
+
|
|
87
|
+
#### `Grape::Middleware::Auth::Base#call` no longer goes through `#call!`
|
|
88
|
+
|
|
89
|
+
`Middleware::Base#call` copied the auth middleware for every request so that `#call!` could keep the env in an instance variable, which only the strategy's block read, to find the endpoint that checks the credentials. The block now takes the endpoint from the request's env, and `#call` answers from the one instance the stack built ([#3018](https://github.com/ruby-grape/grape/pull/3018)). `#call!` still answers a request when called directly.
|
|
90
|
+
|
|
91
|
+
A subclass that overrides `#call!` is therefore no longer called for a request. Override `#call` instead, and keep what a request needs in locals or in the env rather than in instance variables: the instance is shared by every request. A strategy registered with `Grape::Middleware::Auth::Strategies.add` is a middleware of its own and is not affected.
|
|
92
|
+
|
|
93
|
+
#### `Grape::Middleware::Error#call` no longer goes through `#call!`
|
|
94
|
+
|
|
95
|
+
`Grape::Middleware::Error` is in every endpoint's stack, and `Middleware::Base#call` copied it for every request so that `#call!` could keep the env in an ivar, which only rendering an error reads. It now answers from the one instance the stack built, and copies itself only once there is an error to render ([#3016](https://github.com/ruby-grape/grape/pull/3016)). `#call!` still answers a request when called directly.
|
|
96
|
+
|
|
97
|
+
A subclass that overrides `#call!` to wrap every request is therefore no longer called for one. Override `#call` instead, and keep what a request needs in locals or in the env rather than in instance variables: the instance is shared by every request.
|
|
98
|
+
|
|
99
|
+
#### A blank element of an optional Array block is validated when others are not blank
|
|
100
|
+
|
|
101
|
+
An optional Array param given a block used to pass over every empty element, even one sent with others ([#3014](https://github.com/ruby-grape/grape/pull/3014)). So `[{"sku":"A"}, {}]` passed `requires :sku` inside the block, and `[{"sku":"A"}, null]` or `[{"sku":"A"}, ""]` reached the endpoint, where `params[:variants].map { |variant| variant[:sku] }` raised. Such an element is now validated like any other:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
params do
|
|
105
|
+
optional :variants, type: Array do
|
|
106
|
+
requires :sku
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# {"variants": [{"sku": "A"}, {}]}
|
|
111
|
+
# Before: 201, the second variant has no sku
|
|
112
|
+
# After: 400, variants[1][sku] is missing
|
|
113
|
+
|
|
114
|
+
# {"variants": [{"sku": "A"}, null]}
|
|
115
|
+
# Before: 201, the endpoint gets nil
|
|
116
|
+
# After: 400, variants[1] is invalid, variants[1][sku] is missing
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
A blank element that is not a Hash is also rejected in a required Array param whose block declares only optional params: `{"tags": [" ", {"label": "y"}]}` against `requires :tags, type: Array do optional :label end` now answers `tags[0] is invalid`.
|
|
120
|
+
|
|
121
|
+
An Array whose elements are all blank is still passed over, as the whole param is when it is left out, and so is an optional Array scope left out of an element of an Array further up. To keep accepting placeholder elements, leave them out on the client, or declare the param without a block (`optional :variants, type: Array`) and check its elements in the endpoint.
|
|
122
|
+
|
|
123
|
+
#### A header version holding a hyphen is read whole
|
|
124
|
+
|
|
125
|
+
With `version ..., using: :header`, a version holding a hyphen, such as a date (`2024-06-20`) or `2.0-beta`, is now read whole ([#3013](https://github.com/ruby-grape/grape/pull/3013)). The Accept header was split at the version's last hyphen, so the request was routed to the right version but reported the wrong one:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
# version '2024-06-20', using: :header, vendor: 'acme'
|
|
129
|
+
# Accept: application/vnd.acme-2024-06-20+json
|
|
130
|
+
# Before
|
|
131
|
+
version # => "20"
|
|
132
|
+
env['api.vendor'] # => "acme-2024-06"
|
|
133
|
+
# After
|
|
134
|
+
version # => "2024-06-20"
|
|
135
|
+
env['api.vendor'] # => "acme"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
An undeclared version of that shape now fails with `Grape::Exceptions::InvalidVersionHeader` (`API version not found.`), where it failed with `Grape::Exceptions::InvalidAcceptHeader` (`API vendor not found.`).
|
|
139
|
+
|
|
140
|
+
#### An `Integer` param rejects a number with a fractional part
|
|
141
|
+
|
|
142
|
+
A param of `type: Integer` now rejects a number with a fractional part instead of truncating it ([#3011](https://github.com/ruby-grape/grape/pull/3011)). A JSON body's `2.99` became `2` and `-0.5` became `0`, while the same value sent as a string, `"2.99"` in a query, was already rejected. The same applies to `Array[Integer]` and `Set[Integer]` elements. A whole number such as `1.0` still becomes `1`.
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
# {"quantity": 2.99} against `requires :quantity, type: Integer`
|
|
146
|
+
# Before
|
|
147
|
+
201, params[:quantity] == 2
|
|
148
|
+
# After
|
|
149
|
+
400, quantity is invalid
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`types: [Integer, Float]` now answers `2.99` for that body, as it already did for the query string, where it used to answer `2`. To keep accepting and truncating fractional numbers, declare the param `type: Float` and call `to_i` on it, or pass `coerce_with: ->(value) { Integer(value.to_f) }`.
|
|
153
|
+
|
|
154
|
+
#### A required param with `values` rejects `nil` in a nested scope
|
|
155
|
+
|
|
156
|
+
A `requires` param with `values:` now rejects `nil` in a Hash scope, an Array scope or a `given` block, not only at the root ([#3010](https://github.com/ruby-grape/grape/pull/3010)). So does a value that coerces to `nil`, such as `""` for `type: Integer`. Before, only the root scope rejected it, so `{"lines":[{"qty":null}]}` passed the declaration below and reached the endpoint with `qty` set to `nil`:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
params do
|
|
160
|
+
requires :lines, type: Array do
|
|
161
|
+
requires :qty, type: Integer, values: 1..99
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
That request now answers `400` with `lines[0][qty] does not have a valid value`. A nested param that is missing now reports that message after `is missing`, as a root param already did:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
# Before
|
|
170
|
+
address[country] is missing
|
|
171
|
+
# After
|
|
172
|
+
address[country] is missing, address[country] does not have a valid value
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A child scope whose optional parent is absent is still not validated. To keep accepting `nil`, declare the param with `optional` instead.
|
|
176
|
+
|
|
177
|
+
#### A mounted Rack app is an ANY route
|
|
178
|
+
|
|
179
|
+
A Rack app mounted with `mount` (a lambda, Sinatra, Rails engine, anything that is not a Grape API) is now routed as an ANY route, like `route :any`, and keeps the place it was mounted in ([#3012](https://github.com/ruby-grape/grape/pull/3012)). Before, its route carried the method `ANY`, which the router has no routes for. The app was reached only as the fallback that answers `405` and auto `OPTIONS`, and that had these effects:
|
|
180
|
+
|
|
181
|
+
* A `route :any, '*path'` catch-all took every request meant for the app, even one declared after the `mount`.
|
|
182
|
+
* The app's env carried `grape.allowed_methods` (`["OPTIONS", "ANY"]`).
|
|
183
|
+
* A method the app should have answered got `405` when a Grape route used the same path for another method.
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
mount RackApp => '/rack'
|
|
187
|
+
route(:any, '*path') { error!('not found', 404) }
|
|
188
|
+
|
|
189
|
+
# GET /rack/x Before: 404 from the catch-all After: RackApp
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
mount RackApp => '/rack'
|
|
194
|
+
get('rack/status') { 'ok' }
|
|
195
|
+
|
|
196
|
+
# POST /rack/status Before: 405 (Allow: OPTIONS, GET, HEAD) After: RackApp
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The mount's route reports `request_method` as `'*'` instead of `'ANY'`. Code that picks mounted apps out of `API.routes` by that value should compare with `'*'`.
|
|
200
|
+
|
|
201
|
+
#### A validation error without a message says the param is invalid
|
|
202
|
+
|
|
203
|
+
A `Grape::Exceptions::Validation` created without a `message:` now carries the `:invalid` message key and the message `is invalid` ([#2993](https://github.com/ruby-grape/grape/pull/2993)). Before, its `message_key` was `nil`, and its message was the exception's class name, which went into the response. A custom validator with no `default_message_key` that calls `validation_error!(attr_name)` answered:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
# Before
|
|
207
|
+
number Grape::Exceptions::Validation
|
|
208
|
+
# After
|
|
209
|
+
number is invalid
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Code that checked `message_key` for `nil` to find such errors should check for `:invalid` instead. The message can be translated or overridden under `grape.errors.messages.invalid`.
|
|
213
|
+
|
|
214
|
+
#### A `requires` or `optional` block needs exactly one name
|
|
215
|
+
|
|
216
|
+
A `requires` or `optional` given a block now raises `ArgumentError` unless it names exactly one parameter ([#2991](https://github.com/ruby-grape/grape/pull/2991)). Both forms were accepted before, and both declared something other than what they read:
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
# No name: the block's params were declared one level up, and under
|
|
220
|
+
# `optional` they were still required.
|
|
221
|
+
optional type: Hash do
|
|
222
|
+
requires :x
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Several names: `b` was required, but only `a` got the block's params,
|
|
226
|
+
# and `b` was left out of `declared`.
|
|
227
|
+
requires :a, :b, type: Hash do
|
|
228
|
+
requires :x
|
|
229
|
+
end
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Name the one parameter the block describes, and repeat the block for each parameter that shares its shape:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
requires :a, type: Hash do
|
|
236
|
+
requires :x
|
|
237
|
+
end
|
|
238
|
+
requires :b, type: Hash do
|
|
239
|
+
requires :x
|
|
240
|
+
end
|
|
241
|
+
```
|
|
242
|
+
#### A query string Grape does not parse no longer decides the response
|
|
243
|
+
|
|
244
|
+
Grape parsed the query string of every request that had one, only to look for a `?format=` override ([#2972](https://github.com/ruby-grape/grape/pull/2972)). It now parses it only when the string could name that param, and leaves it to whoever reads the params otherwise.
|
|
245
|
+
|
|
246
|
+
A query string Rack refuses to parse -- nested past `Rack::Utils.param_depth_limit`, naming one key as both a value and a hash, or carrying more pairs than the parser accepts -- therefore no longer answers `400` on an endpoint that reads no params. Such a request is answered the way any other request for that route is:
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
get('/ping') { 'pong' }
|
|
250
|
+
|
|
251
|
+
# Before
|
|
252
|
+
GET /ping?a[a][a]...=1 # => 400
|
|
253
|
+
# After
|
|
254
|
+
GET /ping?a[a][a]...=1 # => 200
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
An endpoint that reads the params -- one with a `params` block, or whose code asks for `params` -- still answers `400`, from the parse it asked for.
|
|
258
|
+
|
|
259
|
+
#### A `redirect` body you pass carries the API's content type
|
|
260
|
+
|
|
261
|
+
A body given to `redirect` with `body:` is rendered by the API's formatter, and the response now carries that format's content type instead of `text/plain` ([#2968](https://github.com/ruby-grape/grape/pull/2968)). On a JSON API, `redirect '/there', body: { message: 'moved' }` sent `{"message":"moved"}` labelled `text/plain`; it is now labelled `application/json`. The message Grape generates when no body is given is still plain text under `text/plain`.
|
|
262
|
+
|
|
263
|
+
A client that picked the redirect body's parser by its `Content-Type` now gets the format the body is in; one that relied on `text/plain` for every redirect sees the API's own content type on those that pass a body.
|
|
264
|
+
|
|
265
|
+
#### Every `middleware` entry ends in its block
|
|
266
|
+
|
|
267
|
+
Each entry `API.middleware` returns now ends in the block its `use`, `insert`, `insert_before` or `insert_after` was given, `nil` when there was none ([#2967](https://github.com/ruby-grape/grape/pull/2967)). A Proc passed as the last argument used to be taken for the block, so `use SomeMiddleware, ->(env) { ... }` built the middleware without its callable and raised `ArgumentError`.
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
use SomeMiddleware, 'abc'
|
|
271
|
+
|
|
272
|
+
# Before
|
|
273
|
+
API.middleware # => [[:use, SomeMiddleware, 'abc']]
|
|
274
|
+
# After
|
|
275
|
+
API.middleware # => [[:use, SomeMiddleware, 'abc', nil]]
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Code that reads these entries should take the last element as the block.
|
|
279
|
+
|
|
280
|
+
#### Minimum required Ruby is now 3.3.1
|
|
281
|
+
|
|
282
|
+
Grape no longer supports Ruby 3.3.0; 3.3.1 is now the minimum (`required_ruby_version = '>= 3.3.1'`) ([#2966](https://github.com/ruby-grape/grape/pull/2966)). Ruby 3.3.0 rejects forwarding an anonymous parameter (`*`, `**`, `&`) from inside a block, which Grape does, so Grape could not be loaded on it; 3.3.1 fixed that. Upgrade your runtime to Ruby 3.3.1 or newer before bumping Grape.
|
|
283
|
+
|
|
284
|
+
#### An Array param given a block rejects elements that are not a Hash
|
|
285
|
+
|
|
286
|
+
The block of `requires :items, type: Array do ... end` declares the keys of each element, and an element that is not a Hash now answers `400` with `items[1] is invalid` ([#2957](https://github.com/ruby-grape/grape/pull/2957)). It used to fail only where the block required a key of it, so a block whose params are all optional let a String, a number or a nested Array through to the endpoint, and an Array nested one level too deep in a nested Array scope passed validation and then made `declared` raise. An Array whose elements are all blank — `nil`, `''`, `false`, `[]`, `{}` — is still passed over; a blank element sent with others is not (see above).
|
|
287
|
+
|
|
288
|
+
Where a block does require a key, the element's own error now comes first:
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
# Before
|
|
292
|
+
items[0][key] is missing
|
|
293
|
+
# After
|
|
294
|
+
items[0] is invalid, items[0][key] is missing
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
An API that accepts elements of other shapes in the same list should declare the param without a block (`type: Array`) and check the elements itself.
|
|
298
|
+
|
|
299
|
+
#### A collection with multiple member types rejects what it cannot coerce
|
|
300
|
+
|
|
301
|
+
A param declared as a collection whose members may be of several types — `type: Array[Integer, String]`, or such a collection listed in `types:` — is now validated the way a collection of one type is ([#2955](https://github.com/ruby-grape/grape/pull/2955)). A value that is not an Array, and an Array holding a member none of the types accepts, answer `400` with `is invalid`. The first used to reach the endpoint as `nil`, even under `requires`, and the second with an internal `Grape::Validations::Types::InvalidValue` object where the member was. A `nil` value or an empty String is still `nil`.
|
|
302
|
+
|
|
303
|
+
A `coerce_with` method given to such a param is now handed every value, as it is for `types:`, instead of only an Array. A method that splits a String into the collection works now; one that only ever expected an Array may raise on a String, which answers `400`.
|
|
304
|
+
#### A route's failure entity no longer presents a String error message
|
|
305
|
+
|
|
306
|
+
An entity named for a status in a route's `failure` (or `http_codes`) is now applied to a structured `error!` message only — a Hash or an object — and a String message renders as it does without one ([#2953](https://github.com/ruby-grape/grape/pull/2953)). An entity exposing attributes cannot read any off a String, so `error!('Unauthorized', 401)` under `desc failure: [[401, 'Unauthorized', API::Error]]` raised while rendering and was answered with a `500`; it now answers `401` with the message. An entity written to take the String itself is no longer handed one; give it a Hash to expose from instead:
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
# Before
|
|
310
|
+
class API::Error < Grape::Entity
|
|
311
|
+
expose(:message) { |message, _options| message }
|
|
312
|
+
end
|
|
313
|
+
error!('Unauthorized', 401)
|
|
314
|
+
|
|
315
|
+
# After
|
|
316
|
+
class API::Error < Grape::Entity
|
|
317
|
+
expose :message
|
|
318
|
+
end
|
|
319
|
+
error!({ message: 'Unauthorized' }, 401)
|
|
320
|
+
```
|
|
321
|
+
#### `lint!` checks every response once, from the API that is served
|
|
322
|
+
|
|
323
|
+
`lint!` and `Grape.config.lint` used to put `Rack::Lint` in each endpoint's stack, which missed two kinds of response: the 404 the router answers for a path nothing matches, and those of a Rack app mounted with `mount`, which the router calls directly. A single `Rack::Lint` around the router now checks every response the API gives ([#2960](https://github.com/ruby-grape/grape/pull/2960)). Two things follow from that.
|
|
324
|
+
|
|
325
|
+
`lint!` counts only at the top level of the API that is served. Declared inside a namespace, or in an API mounted into another one, it no longer does anything: declare it on the API you serve, or set `Grape.config.lint`.
|
|
326
|
+
|
|
327
|
+
A test suite that lints its API may start raising `Rack::Lint::LintError` where a mounted Rack app breaks the Rack SPEC — returning a frozen headers Hash, for instance — or where a spec calls the API with an incomplete env:
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
# Before: passed under Grape.config.lint
|
|
331
|
+
MyAPI.call({})
|
|
332
|
+
|
|
333
|
+
# After
|
|
334
|
+
MyAPI.call(Rack::MockRequest.env_for('/'))
|
|
335
|
+
```
|
|
336
|
+
#### The header versioner's `api.*` env values are frozen
|
|
337
|
+
|
|
338
|
+
`version ..., using: :header` now answers the Accept headers most requests send from a table built once, so every request sending the same header is handed the same parsed media type ([#2936](https://github.com/ruby-grape/grape/pull/2936)). The strings it writes into the env — `api.type`, `api.subtype`, `api.vendor`, `api.version` and `api.format` — are therefore frozen, as is `Grape::Util::MediaType` itself. Code that altered one of them in place now raises `FrozenError`; build a new String instead:
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
# Before
|
|
342
|
+
env['api.version'] << '-beta'
|
|
343
|
+
|
|
344
|
+
# After
|
|
345
|
+
env['api.version'] = "#{env['api.version']}-beta"
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
#### `Grape::Util::Lazy` no longer builds a tree of value wrappers
|
|
349
|
+
|
|
350
|
+
A configuration read is now a single `Grape::Util::Lazy::Value` holding the configuration it was read from and the path of keys walked into it, rather than a node in a tree mirroring the whole `with:` Hash ([#2950](https://github.com/ruby-grape/grape/pull/2950)). `Grape::Util::Lazy::ValueHash`, `Grape::Util::Lazy::ValueArray` and `Grape::Util::Lazy::ValueEnumerable` are gone, as are `Value#reached_by` and `Value#fetch`; `Value#access_keys` is now `Value#path`. The `configuration` DSL is unchanged — only code reaching into these internals is affected.
|
|
351
|
+
|
|
352
|
+
`Grape::Util::EndpointConfiguration#evaluate` also answers the configuration Hash itself now, instead of rebuilding a copy on each call. Mutating what `configuration` returns inside an endpoint therefore persists for later requests, where it used to be discarded:
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
# Before: mutated a throwaway copy
|
|
356
|
+
# After: mutates the configuration every later request reads
|
|
357
|
+
get '/x' do
|
|
358
|
+
configuration[:list] << 'item'
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Treat it as read-only, or copy it first
|
|
362
|
+
get '/x' do
|
|
363
|
+
list = configuration[:list].dup
|
|
364
|
+
list << 'item'
|
|
365
|
+
end
|
|
366
|
+
```
|
|
367
|
+
|
|
4
368
|
### Upgrading to >= 4.0.0
|
|
5
369
|
|
|
6
370
|
#### A positional options Hash is no longer accepted by `auth`, `http_basic` or `desc`
|
data/grape.gemspec
CHANGED
|
@@ -29,5 +29,5 @@ Gem::Specification.new do |s|
|
|
|
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.3'
|
|
32
|
+
s.required_ruby_version = '>= 3.3.1'
|
|
33
33
|
end
|
data/lib/grape/api/instance.rb
CHANGED
|
@@ -15,6 +15,8 @@ module Grape
|
|
|
15
15
|
extend Grape::DSL::Middleware
|
|
16
16
|
extend Grape::DSL::RequestResponse
|
|
17
17
|
extend Grape::DSL::Routing
|
|
18
|
+
extend Grape::DSL::Mounting
|
|
19
|
+
extend Grape::DSL::Versioning
|
|
18
20
|
extend Grape::DSL::Helpers
|
|
19
21
|
extend Grape::Middleware::Auth::DSL
|
|
20
22
|
|
|
@@ -94,7 +96,6 @@ module Grape
|
|
|
94
96
|
def inherited(subclass)
|
|
95
97
|
super
|
|
96
98
|
subclass.reset!
|
|
97
|
-
subclass.logger logger.clone
|
|
98
99
|
end
|
|
99
100
|
end
|
|
100
101
|
|
|
@@ -111,23 +112,33 @@ module Grape
|
|
|
111
112
|
|
|
112
113
|
@router.compile!
|
|
113
114
|
@router.freeze
|
|
114
|
-
|
|
115
|
+
# One Rack::Head around the router strips the body of every HEAD
|
|
116
|
+
# response the API gives, including the router's own 404 and a mounted
|
|
117
|
+
# Rack app's, which go through no endpoint's stack. Rack::Lint goes
|
|
118
|
+
# outside it, where it sees the response that is served.
|
|
119
|
+
@app = Rack::Head.new(@router)
|
|
120
|
+
@app = Rack::Lint.new(@app) if lint?
|
|
121
|
+
@cascade = self.class.cascade
|
|
115
122
|
end
|
|
116
123
|
|
|
117
124
|
# Handle a request. See Rack documentation for what `env` is.
|
|
118
125
|
def call(env)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
response = @app.call(env)
|
|
127
|
+
return response if @cascade
|
|
128
|
+
|
|
129
|
+
headers = response[1]
|
|
130
|
+
# A Grape::Util::Header folds case the way the copy below does, so it can
|
|
131
|
+
# answer for it; any other Hash cannot, and still goes through the copy.
|
|
132
|
+
return response if headers.is_a?(Grape::Util::Header) && !headers.key?('X-Cascade')
|
|
133
|
+
|
|
134
|
+
# +merge!+, not +merge+: the latter is a `dup` plus a `merge!`, so the
|
|
135
|
+
# Header built on this line would be allocated only to be discarded.
|
|
136
|
+
# The copy stays because +headers+ can come from a mounted Rack app,
|
|
137
|
+
# which is free to hand back a frozen or shared Hash that the delete
|
|
138
|
+
# below must not reach into.
|
|
139
|
+
headers = Grape::Util::Header.new.merge!(headers)
|
|
140
|
+
headers.delete('X-Cascade')
|
|
141
|
+
[response[0], headers, response[2]]
|
|
131
142
|
end
|
|
132
143
|
|
|
133
144
|
# Some requests may return a HTTP 404 error if grape cannot find a matching
|
|
@@ -136,7 +147,7 @@ module Grape
|
|
|
136
147
|
# looking for a matching route on other resources.
|
|
137
148
|
#
|
|
138
149
|
# In some applications (e.g. mounting grape on rails), one might need to trap
|
|
139
|
-
# errors from reaching upstream. This is
|
|
150
|
+
# errors from reaching upstream. This is effectively done by unsetting
|
|
140
151
|
# X-Cascade. Default :cascade is true.
|
|
141
152
|
#
|
|
142
153
|
# Resolved in the constructor rather than per request: answering it walks
|
|
@@ -167,29 +178,45 @@ module Grape
|
|
|
167
178
|
setting = self.class.inheritable_setting
|
|
168
179
|
routes_by_regexp = all_routes.group_by(&:pattern_regexp)
|
|
169
180
|
|
|
170
|
-
# Build the configuration
|
|
181
|
+
# Build the configuration from the collection of methods supported.
|
|
171
182
|
routes_by_regexp.each_value do |routes|
|
|
172
183
|
next if routes.any? { |route| route.request_method == '*' }
|
|
173
184
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
185
|
+
associate_greedy_route(routes, setting)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
177
188
|
|
|
178
|
-
|
|
179
|
-
|
|
189
|
+
# The greedy route answering 405 and auto-OPTIONS for the path +routes+
|
|
190
|
+
# share. Most of the configuration is taken from the last endpoint.
|
|
191
|
+
def associate_greedy_route(routes, setting)
|
|
192
|
+
last_route = routes.last
|
|
193
|
+
allowed_methods = allowed_methods_for(routes, setting)
|
|
180
194
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
195
|
+
allow_header = setting.do_not_route_options? ? allowed_methods : [Rack::OPTIONS] | allowed_methods
|
|
196
|
+
last_route.app.options_route_enabled = true unless setting.do_not_route_options? || allowed_methods.include?(Rack::OPTIONS)
|
|
197
|
+
|
|
198
|
+
greedy_route = Grape::Router::GreedyRoute.new(last_route.pattern, endpoint: last_route.app, allow_header:)
|
|
199
|
+
@router.associate_routes(greedy_route)
|
|
184
200
|
end
|
|
185
201
|
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return
|
|
202
|
+
# The methods +routes+ answer, with HEAD alongside GET unless the API
|
|
203
|
+
# declared +do_not_route_head!+.
|
|
204
|
+
def allowed_methods_for(routes, setting)
|
|
205
|
+
allowed_methods = routes.map(&:request_method)
|
|
206
|
+
return allowed_methods if setting.do_not_route_head? || !allowed_methods.include?(Rack::GET)
|
|
207
|
+
|
|
208
|
+
allowed_methods | [Rack::HEAD]
|
|
209
|
+
end
|
|
191
210
|
|
|
192
|
-
|
|
211
|
+
# +lint!+ declared at the top of this API, or Grape.config.lint: one
|
|
212
|
+
# Rack::Lint around the router then checks every response the API gives
|
|
213
|
+
# -- an endpoint's, the 404 the router answers itself, a mounted Rack
|
|
214
|
+
# app's -- exactly once. Linting inside each endpoint's stack instead
|
|
215
|
+
# missed the last two, and wrapping a second Rack::Lint around those
|
|
216
|
+
# breaks a middleware that calls +to_ary+ on the body, as Rack::ETag does.
|
|
217
|
+
# Called once, from the constructor.
|
|
218
|
+
def lint?
|
|
219
|
+
self.class.inheritable_setting.lint? || Grape.config.lint
|
|
193
220
|
end
|
|
194
221
|
end
|
|
195
222
|
end
|
data/lib/grape/api.rb
CHANGED
|
@@ -7,21 +7,28 @@ module Grape
|
|
|
7
7
|
# Marks this and every subclass as a mountable Grape app (see Grape::Mountable).
|
|
8
8
|
extend Grape::Mountable
|
|
9
9
|
|
|
10
|
-
# Class methods that we want to call on the API rather than on the API object
|
|
11
|
-
|
|
10
|
+
# Class methods that we want to call on the API rather than on the API object.
|
|
11
|
+
# +inherit_settings+ is protected on the base instance, and +.methods+ answers
|
|
12
|
+
# protected methods too, so it has to be named here for {.override_all_methods!}
|
|
13
|
+
# to leave it alone. +base+ is read on the API class itself whenever a mount
|
|
14
|
+
# is refreshed, so recording it would refresh every mount below it again.
|
|
15
|
+
NON_OVERRIDABLE = %i[base base= base_instance? call change! configuration compile! inherit_settings recognize_path reset! routes top_level_setting].freeze
|
|
16
|
+
|
|
17
|
+
# DSL methods that answer a setting when called with nothing to set -- no
|
|
18
|
+
# argument, keyword or block -- and change none. Such a call is a read, and
|
|
19
|
+
# is answered without being recorded: as a setup step it was replayed on
|
|
20
|
+
# every mount and, like any step, refreshed every mount made before it,
|
|
21
|
+
# which discards the compiled API, so a read after boot made the next
|
|
22
|
+
# request recompile it. +helpers+ is left out: called bare it includes the
|
|
23
|
+
# helpers in scope and calls +change!+ itself.
|
|
24
|
+
SETTING_READERS = %i[
|
|
25
|
+
auth cascade content_types default_error_formatter default_error_status default_format endpoints format
|
|
26
|
+
group inheritable_setting logger middleware namespace prefix resource resources segment version versions
|
|
27
|
+
].freeze
|
|
12
28
|
|
|
13
29
|
Helpers = Grape::DSL::Helpers::BaseHelper
|
|
14
30
|
|
|
15
|
-
|
|
16
|
-
VALUES = [true, false].freeze
|
|
17
|
-
private_constant :VALUES
|
|
18
|
-
|
|
19
|
-
def self.build(val)
|
|
20
|
-
return unless VALUES.include?(val)
|
|
21
|
-
|
|
22
|
-
new
|
|
23
|
-
end
|
|
24
|
-
end
|
|
31
|
+
Boolean = Class.new
|
|
25
32
|
|
|
26
33
|
class << self
|
|
27
34
|
extend Forwardable
|
|
@@ -30,12 +37,30 @@ module Grape
|
|
|
30
37
|
|
|
31
38
|
delegate_missing_to :base_instance
|
|
32
39
|
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
40
|
+
# Every NON_OVERRIDABLE name the base instance answers is forwarded here
|
|
41
|
+
# rather than left to +delegate_missing_to+, which stays for the names
|
|
42
|
+
# this list cannot know: anything defined on Grape::API::Instance after
|
|
43
|
+
# an API class was created, since {.override_all_methods!} copies the
|
|
44
|
+
# methods it finds at that moment.
|
|
45
|
+
#
|
|
46
|
+
# +inherit_settings+ is left out: it is protected on the base instance, so
|
|
47
|
+
# a delegator would raise NoMethodError just as the missing one does.
|
|
48
|
+
# +call+ is written out below.
|
|
49
|
+
def_delegators :base_instance, :new, :configuration, :change!, :compile!, :recognize_path, :routes,
|
|
50
|
+
:base, :base=, :base_instance?, :reset!, :top_level_setting
|
|
51
|
+
|
|
52
|
+
# The interface point between Rack and Grape; it accepts a request from
|
|
53
|
+
# Rack and ultimately returns an array of three values: the status, the
|
|
54
|
+
# headers, and the body. See [the rack specification]
|
|
36
55
|
# (https://github.com/rack/rack/blob/main/SPEC.rdoc) for more.
|
|
37
56
|
# NOTE: This will only be called on an API directly mounted on RACK
|
|
38
|
-
|
|
57
|
+
#
|
|
58
|
+
# Not delegated with the rest because it runs on every request, and a
|
|
59
|
+
# Forwardable delegator checks its target with +defined?+ and forwards
|
|
60
|
+
# through +...+ each time: about as much again as the call it makes.
|
|
61
|
+
def call(env)
|
|
62
|
+
base_instance.call(env)
|
|
63
|
+
end
|
|
39
64
|
|
|
40
65
|
# Initialize the instance variables on the remountable class, and the base_instance
|
|
41
66
|
# an instance that will be used to create the set up but will not be mounted
|
|
@@ -46,11 +71,16 @@ module Grape
|
|
|
46
71
|
@base_instance = mount_instance
|
|
47
72
|
end
|
|
48
73
|
|
|
49
|
-
# Redefines all methods so that are forwarded to add_setup and be recorded
|
|
74
|
+
# Redefines all methods so that are forwarded to add_setup and be recorded.
|
|
75
|
+
# A read (see SETTING_READERS) is answered by the last instance, which is
|
|
76
|
+
# where add_setup takes its answer from, without being recorded.
|
|
50
77
|
def override_all_methods!
|
|
51
78
|
(base_instance.methods - Class.methods - NON_OVERRIDABLE).each do |method_override|
|
|
52
79
|
define_singleton_method(method_override) do |*args, **kwargs, &block|
|
|
53
|
-
|
|
80
|
+
step = { method: method_override, args:, kwargs:, block: }
|
|
81
|
+
return replay_step_on(@instances.last, **step) if setting_read?(method_override, args, kwargs, block)
|
|
82
|
+
|
|
83
|
+
add_setup(**step)
|
|
54
84
|
end
|
|
55
85
|
end
|
|
56
86
|
end
|
|
@@ -136,6 +166,10 @@ module Grape
|
|
|
136
166
|
evaluate_arguments(instance.configuration, response).first
|
|
137
167
|
end
|
|
138
168
|
|
|
169
|
+
def setting_read?(method, args, kwargs, block)
|
|
170
|
+
SETTING_READERS.include?(method) && args.empty? && kwargs.empty? && block.nil?
|
|
171
|
+
end
|
|
172
|
+
|
|
139
173
|
# Skips steps that contain arguments to be lazily executed (on re-mount time)
|
|
140
174
|
def skip_immediate_run?(instance, args, kwargs)
|
|
141
175
|
instance.base_instance? &&
|