openapi_first 1.3.1 → 1.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +310 -0
- data/LICENSE.txt +21 -0
- data/README.md +262 -0
- data/lib/openapi_first/configuration.rb +2 -2
- data/lib/openapi_first/definition/responses.rb +4 -0
- data/lib/openapi_first/json_refs.rb +143 -0
- data/lib/openapi_first/middlewares/request_validation.rb +4 -1
- data/lib/openapi_first/middlewares/response_validation.rb +4 -0
- data/lib/openapi_first/plugins.rb +6 -1
- data/lib/openapi_first/version.rb +1 -1
- data/lib/openapi_first.rb +6 -17
- metadata +10 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d004ac9e317cce0a7d9df95e49b3abbe435e9dab2f23d91880b29cb670ece06f
|
4
|
+
data.tar.gz: c8a1b0ee8238adbf6869aa4a81f642fa0bae0091eabab63417ee142df50ded47
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e937d066ee559653a60c28b910da5f02def4b73cbb653673c89b2402f96f2dbe79aad482e4c92c23270d0cf9b9e8b35b2423770abc89590056bcd9b5d16332d
|
7
|
+
data.tar.gz: edab0f813f6e98f2db2e245d6d306b0218e678a5ff0e8b22330d6bb69354011d4c254a9e38b2bf15869294b7f9e5d2b19eb7511079665f414b15f2f3da8b2357
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,310 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## Unreleased
|
4
|
+
|
5
|
+
## 1.3.2
|
6
|
+
|
7
|
+
### Changed
|
8
|
+
|
9
|
+
- The response definition is found even if the status is defined as an Integer instead of a String. This is not provided for in the OAS specification, but is often done this way, because of YAML.
|
10
|
+
|
11
|
+
### Fixed
|
12
|
+
|
13
|
+
- Reduced initial load time for composed API descriptions [#232](https://github.com/ahx/openapi_first/pull/232)
|
14
|
+
- Chore: Add Readme back to gem. Add link to docs.
|
15
|
+
|
16
|
+
## 1.3.1
|
17
|
+
|
18
|
+
- Fixed warning about duplicated constant
|
19
|
+
|
20
|
+
## 1.3.0
|
21
|
+
|
22
|
+
No breaking changes
|
23
|
+
|
24
|
+
New features:
|
25
|
+
|
26
|
+
- Added new API: `Definition#validate_request`, `Definition#validate_response`, `RuntimeRequest#validate_response` (see readme) [#222](https://github.com/ahx/openapi_first/pull/222)
|
27
|
+
|
28
|
+
Fixes:
|
29
|
+
|
30
|
+
- Manual response validation (without the middleware) just works in Rails' request tests now. [#224](https://github.com/ahx/openapi_first/pull/224)
|
31
|
+
|
32
|
+
## 1.2.0
|
33
|
+
|
34
|
+
No breaking changes
|
35
|
+
|
36
|
+
- Added `OpenapiFirst.parse(hash)` to load ("parse") a resolved/de-referenced Hash
|
37
|
+
- Added support for unescaped special characters in the path params (https://github.com/ahx/openapi_first/pull/217)
|
38
|
+
- Added `operation` to `RuntimeRequest` by [@MrBananaLord](https://github.com/ahx/openapi_first/pull/216)
|
39
|
+
|
40
|
+
## 1.1.1
|
41
|
+
|
42
|
+
- Fix reading response body for example when running Rails (`ActionDispatch::Response::RackBody`)
|
43
|
+
- Add `known?`, `status`, `body`, `headers`, `content_type` methods to inspect the parsed response (`RuntimeResponse`)
|
44
|
+
- Add `OpenapiFirst::ParseError` which is raised by low-level interfaces like `request.body` if the body could not be parsed.
|
45
|
+
- Add "code" field to errors in JSON:API error response
|
46
|
+
|
47
|
+
## 1.1.0 (yanked)
|
48
|
+
|
49
|
+
## 1.0.0
|
50
|
+
|
51
|
+
- Breaking: The default error uses application/problem+json content-type
|
52
|
+
- Breaking: Moved rack middlewares to OpenapiFirst::Middlewares
|
53
|
+
- Breaking: Rename OpenapiFirst::ResponseInvalid to OpenapiFirst::ResponseInvalidError
|
54
|
+
- Breaking: Remove OpenapiFirst::Router
|
55
|
+
- Breaking: Remove `env[OpenapiFirst::OPERATION]`. Use `env[OpenapiFirst::REQUEST]` instead.
|
56
|
+
- Breaking: Remove `env[OpenapiFirst::REQUEST_BODY]`, `env[OpenapiFirst::PARAMS]`. Use `env[OpenapiFirst::REQUEST].body env[OpenapiFirst::REQUEST].params` instead.
|
57
|
+
- Add interface to validate requests / responses without middlewares (see "Manual validation" in README)
|
58
|
+
- Add OpenapiFirst.configure
|
59
|
+
- Add OpenapiFirst.register, OpenapiFirst.plugin
|
60
|
+
- Fix response header validation with Rack 3
|
61
|
+
- Fixed: Add support for paths like `/{a}..{b}`
|
62
|
+
|
63
|
+
## 1.0.0.beta6
|
64
|
+
|
65
|
+
- Fix: Make response header validation work with rack 3
|
66
|
+
- Refactor router
|
67
|
+
- Remove dependency hanami-router
|
68
|
+
- PathItem and Operation for a request can be found by calling methods on the Definitnion
|
69
|
+
- Fixed https://github.com/ahx/openapi_first/issues/155
|
70
|
+
- Breaking / Regression: A paths like /pets/{from}-{to} if there is a path "/pets/{id}"
|
71
|
+
|
72
|
+
## 1.0.0.beta5
|
73
|
+
|
74
|
+
- Added: `OpenapiFirst::Config.default_options=` to set default options globally
|
75
|
+
- Added: You can define custom error responses by subclassing `OpenapiFirst::ErrorResponse` and register it via `OpenapiFirst.register_error_response(name, MyCustomErrorResponse)`
|
76
|
+
|
77
|
+
## 1.0.0.beta4
|
78
|
+
|
79
|
+
- Update json_schemer to version 2.0
|
80
|
+
- Breaking: Requires Ruby 3.1 or later
|
81
|
+
- Added: Parameters are available at `env[OpenapiFirst::PATH_PARAMS]`, `env[OpenapiFirst::QUERY_PARAMS]`, `env[OpenapiFirst::HEADER_PARAMS]`, `env[OpenapiFirst::COOKIE_PARAMS]` in case you need to access them separately. Merged path and query parameters are still available at `env[OpenapiFirst::PARAMS]`
|
82
|
+
- Breaking / Added: ResponseValidation now validates response headers
|
83
|
+
- Breaking / Added: RequestValidation now validates cookie, path and header parameters
|
84
|
+
- Breaking: multipart File uploads are now read and then validated
|
85
|
+
- Breaking: Remove OpenapiFirst.env method
|
86
|
+
- Breaking: Request validation returns 400 instead of 415 if request body is required, but empty
|
87
|
+
|
88
|
+
## 1.0.0.beta3
|
89
|
+
|
90
|
+
- Remove obsolete dependency: deep_merge
|
91
|
+
- Remove obsolete dependency: hanami-utils
|
92
|
+
|
93
|
+
## 1.0.0.beta2
|
94
|
+
|
95
|
+
- Fixed dependencies. Remove unused code.
|
96
|
+
|
97
|
+
## 1.0.0.beta1
|
98
|
+
|
99
|
+
- Removed: `OpenapiFirst::Responder` and `OpenapiFirst::RackResponder`
|
100
|
+
- Removed: `OpenapiFirst.app` and `OpenapiFirst.middleware`
|
101
|
+
- Removed: `OpenapiFirst::Coverage`
|
102
|
+
- Breaking: Parsed query and path parameters are available at `env[OpenapiFirst::PARAMS]`(or `env['openapi.params']`) instead of `OpenapiFirst::PARAMETERS`.
|
103
|
+
- Breaking: Request body and parameters now use string keys instead of symbols!
|
104
|
+
- Breaking: Query parameters are now parsed exactly like in the API description via the openapi_parameters gem. This means a couple of things:
|
105
|
+
- Query parameters now support `explode: true` (default) and `explode: false` for array and object parameters.
|
106
|
+
- Query parameters with brackets like 'filter[tag]' are no longer deconstructed into nested hashes, but accessible via the `filter[tag]` key.
|
107
|
+
- Query parameters are no longer interpreted as `style: deepObject` by default. If you want to use `style: deepObject`, for example to pass a nested hash as a query parameter like `filter[tag]`, you have to set `style: deepObject` explicitly.
|
108
|
+
- Path parameters are now parsed exactly as in the API description via the openapi_parameters gem.
|
109
|
+
|
110
|
+
## 0.21.0
|
111
|
+
|
112
|
+
- Fix: Query parameter validation does not fail if header parameters are defined (Thanks to [JF Lalonde](https://github.com/JF-Lalonde))
|
113
|
+
- Update Ruby dependency to >= 3.0.5
|
114
|
+
- Handle simple form-data in request bodies (see https://github.com/ahx/openapi_first/issues/149)
|
115
|
+
- Update to hanami-router 2.0.0 stable
|
116
|
+
|
117
|
+
## 0.20.0
|
118
|
+
|
119
|
+
- You can pass a filepath to `spec:` now so you no longer have to call `OpenapiFirst.load` anymore.
|
120
|
+
- Router is optional now.
|
121
|
+
You no longer have to add `Router` to your middleware stack. You still can add it to customize behaviour by setting options, but you no longer have to add it.
|
122
|
+
If you don't add the Router, make sure you pass `spec:` to your request/response validation middleware.
|
123
|
+
- Support "4xx" and "4XX" response definitions.
|
124
|
+
(4XX is defined in the standard, but 2xx is used in the wild as well 🦁.)
|
125
|
+
- Removed warning about missing operationId, because operationId is not used until the Responder is used.
|
126
|
+
- Raise HandlerNotFoundError when handler cannot be found
|
127
|
+
|
128
|
+
## 0.19.0
|
129
|
+
|
130
|
+
- Add `RackResponder`
|
131
|
+
|
132
|
+
- BREAKING CHANGE: Handler classes are now instantiated only once without any arguments and the same instance is called on each following call/request.
|
133
|
+
|
134
|
+
## 0.18.0
|
135
|
+
|
136
|
+
Yanked. No useful changes.
|
137
|
+
|
138
|
+
## 0.17.0
|
139
|
+
|
140
|
+
- BREAKING CHANGE: Use a Hash instead of named arguments for middleware options for better compatibility
|
141
|
+
Using named arguments is actually not supported in Rack.
|
142
|
+
|
143
|
+
## 0.16.1
|
144
|
+
|
145
|
+
- Pin hanami-router version, because alpha6 is broken.
|
146
|
+
|
147
|
+
## 0.16.0
|
148
|
+
|
149
|
+
- Support status code wildcards like "2XX", "4XX"
|
150
|
+
|
151
|
+
## 0.15.0
|
152
|
+
|
153
|
+
- Populate default parameter values
|
154
|
+
|
155
|
+
## 0.14.3
|
156
|
+
|
157
|
+
- Use json_refs to resolve OpenAPI file. This removes oas_parser and ActiveSupport from list of dependencies
|
158
|
+
|
159
|
+
## 0.14.2
|
160
|
+
|
161
|
+
- Empty query parameters are parsed and request validation returns 400 if an empty string is not allowed. Note that this does not look at `allowEmptyValue` in any way, because allowEmptyValue is deprecated.
|
162
|
+
|
163
|
+
## 0.14.1
|
164
|
+
|
165
|
+
- Fix: Don't mix path- and operation-level parameters for request validation
|
166
|
+
|
167
|
+
## 0.14.0
|
168
|
+
|
169
|
+
- Handle custom x-handler field in the API description to find a handler method not based on operationId
|
170
|
+
- Add `resolver` option to provide a custom resolver to find a handler method
|
171
|
+
|
172
|
+
## 0.13.3
|
173
|
+
|
174
|
+
- Better error message if string does not match format
|
175
|
+
- readOnly and writeOnly just works when used inside allOf
|
176
|
+
|
177
|
+
## 0.13.2
|
178
|
+
|
179
|
+
- Return indicator (`source: { parameter: 'list/1' }`) in error response body when array item in query parameter is invalid
|
180
|
+
|
181
|
+
## 0.13.0
|
182
|
+
|
183
|
+
- Add support for arrays in query parameters (style: form, explode: false)
|
184
|
+
- Remove warning when handler is not implemented
|
185
|
+
|
186
|
+
## 0.12.5
|
187
|
+
|
188
|
+
- Add `not_found: :continue` option to Router to make it do nothing if request is unknown
|
189
|
+
|
190
|
+
## 0.12.4
|
191
|
+
|
192
|
+
- content-type is found while ignoring additional content-type parameters (`application/json` is found when request/response content-type is `application/json; charset=UTF8`)
|
193
|
+
- Support wildcard mime-types when finding the content-type
|
194
|
+
|
195
|
+
## 0.12.3
|
196
|
+
|
197
|
+
- Add `response_validation:`, `router_raise_error` options to standalone mode.
|
198
|
+
|
199
|
+
## 0.12.2
|
200
|
+
|
201
|
+
- Allow response to have no media type object specified
|
202
|
+
|
203
|
+
## 0.12.1
|
204
|
+
|
205
|
+
- Fix response when handler returns 404 or 405
|
206
|
+
- Don't validate the response content if status is 204 (no content)
|
207
|
+
|
208
|
+
## 0.12.0
|
209
|
+
|
210
|
+
- Change `ResponseValidator` to raise an exception if it found a problem
|
211
|
+
- Params have symbolized keys now
|
212
|
+
- Remove `not_found` option from Router. Return 405 if HTTP verb is not allowed (via Hanami::Router)
|
213
|
+
- Add `raise_error` option to OpenapiFirst.app (false by default)
|
214
|
+
- Add ResponseValidation to OpenapiFirst.app if raise_error option is true
|
215
|
+
- Rename `raise` option to `raise_error`
|
216
|
+
- Add `raise_error` option to RequestValidation middleware
|
217
|
+
- Raise error if handler could not be found by Responder
|
218
|
+
- Add `Operation#name` that returns a human readable name for an operation
|
219
|
+
|
220
|
+
## 0.11.0
|
221
|
+
|
222
|
+
- Raise error if you forgot to add the Router middleware
|
223
|
+
- Make OpenapiFirst.app raise an error in test env when request path is not specified
|
224
|
+
- Rename OperationResolver to Responder
|
225
|
+
- Add ResponseValidation middleware that validates the response body
|
226
|
+
- Add `raise` option to Router middleware to raise an error if request could not be found in the API description similar to committee's raise option.
|
227
|
+
- Move namespace option from Router to OperationResolver
|
228
|
+
|
229
|
+
## 0.10.2
|
230
|
+
|
231
|
+
- Return 400 if request body has invalid JSON ([issue](https://github.com/ahx/openapi_first/issues/73)) thanks Thomas Frütel
|
232
|
+
|
233
|
+
## 0.10.1
|
234
|
+
|
235
|
+
- Fix duplicated key in `required` when generating JSON schema for `some[thing]` parameters
|
236
|
+
|
237
|
+
## 0.10.0
|
238
|
+
|
239
|
+
- Add support for query parameters named `"some[thing]"` ([issue](https://github.com/ahx/openapi_first/issues/40))
|
240
|
+
|
241
|
+
## 0.9.0
|
242
|
+
|
243
|
+
- Make request validation usable standalone
|
244
|
+
|
245
|
+
## 0.8.0
|
246
|
+
|
247
|
+
- Add merged parameter and request body available to env at `env[OpenapiFirst::INBOX]` in request validation
|
248
|
+
- Path and query parameters with `type: boolean` now get converted to `true`/`false`
|
249
|
+
- Rename `OpenapiFirst::PARAMS` to `OpenapiFirst::PARAMETERS`
|
250
|
+
|
251
|
+
## 0.7.1
|
252
|
+
|
253
|
+
- Add missing `require` to work with new version of `oas_parser`
|
254
|
+
|
255
|
+
## 0.7.0
|
256
|
+
|
257
|
+
- Make use of hanami-router, because it's fast
|
258
|
+
- Remove option `allow_unknown_query_paramerters`
|
259
|
+
- Move the namespace option to Router
|
260
|
+
- Convert numeric path and query parameters to `Integer` or `Float`
|
261
|
+
- Pass the Rack env if your action class' initializers accepts an argument
|
262
|
+
- Respec rack's `env['SCRIPT_NAME']` in router
|
263
|
+
- Add MIT license
|
264
|
+
|
265
|
+
## 0.6.10
|
266
|
+
|
267
|
+
- Bugfix: params.env['unknown'] now returns `nil` as expected. Thanks @tristandruyen.
|
268
|
+
|
269
|
+
## 0.6.9
|
270
|
+
|
271
|
+
- Removed radix tree, because of a bug (https://github.com/namusyaka/r2ree-ruby/issues/2)
|
272
|
+
|
273
|
+
## 0.6.8
|
274
|
+
|
275
|
+
- Performance: About 25% performance increase (i/s) with help of c++ based radix-tree and some optimizations
|
276
|
+
- Update dependencies
|
277
|
+
|
278
|
+
## 0.6.7
|
279
|
+
|
280
|
+
- Fix: version number of oas_parser
|
281
|
+
|
282
|
+
## 0.6.6
|
283
|
+
|
284
|
+
- Remove warnings for Ruby 2.7
|
285
|
+
|
286
|
+
## 0.6.5
|
287
|
+
|
288
|
+
- Merge QueryParameterValidation and ReqestBodyValidation middlewares into RequestValidation
|
289
|
+
- Rename option to `allow_unknown_query_paramerters`
|
290
|
+
|
291
|
+
## 0.6.4
|
292
|
+
|
293
|
+
- Fix: Rewind request body after reading
|
294
|
+
|
295
|
+
## 0.6.3
|
296
|
+
|
297
|
+
- Add option to parse only certain paths from OAS file
|
298
|
+
|
299
|
+
## 0.6.2
|
300
|
+
|
301
|
+
- Add support to map operationIds like `things#index` or `web.things_index`
|
302
|
+
|
303
|
+
## 0.6.1
|
304
|
+
|
305
|
+
- Make ResponseValidator errors easier to read
|
306
|
+
|
307
|
+
## 0.6.0
|
308
|
+
|
309
|
+
- Set the content-type based on the OpenAPI description [#29](https://github.com/ahx/openapi-first/pull/29)
|
310
|
+
- Add CHANGELOG 📝
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Andreas Haller
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,262 @@
|
|
1
|
+
# openapi_first
|
2
|
+
|
3
|
+
OpenapiFirst helps to implement HTTP APIs based on an [OpenAPI](https://www.openapis.org/) API description. It supports OpenAPI 3.0 and 3.1. It offers request and response validation and it ensures that your implementation follows exactly the API description.
|
4
|
+
|
5
|
+
## Contents
|
6
|
+
|
7
|
+
<!-- TOC -->
|
8
|
+
|
9
|
+
- [Rack Middlewares](#rack-middlewares)
|
10
|
+
- [Request validation](#request-validation)
|
11
|
+
- [Response validation](#response-validation)
|
12
|
+
- [Manual use](#manual-use)
|
13
|
+
- [Validate request](#validate-request)
|
14
|
+
- [Validate response](#validate-response)
|
15
|
+
- [Configuration](#configuration)
|
16
|
+
- [Framework integration](#framework-integration)
|
17
|
+
- [Alternatives](#alternatives)
|
18
|
+
- [Development](#development)
|
19
|
+
- [Benchmarks](#benchmarks)
|
20
|
+
- [Contributing](#contributing)
|
21
|
+
|
22
|
+
<!-- /TOC -->
|
23
|
+
|
24
|
+
## Rack Middlewares
|
25
|
+
|
26
|
+
All middlewares add a _request_ object to the current Rack env at `env[OpenapiFirst::REQUEST]`), which is in an instance of `OpenapiFirst::RuntimeRequest` that responds to `.params`, `.parsed_body` etc.
|
27
|
+
|
28
|
+
This gives you access to the converted request parameters and body exaclty as described in your API description instead of relying on Rack alone to parse the request. This only includes query parameters that are defined in the API description. It supports every [`style` and `explode` value as described](https://spec.openapis.org/oas/latest.html#style-examples) in the OpenAPI 3.0 and 3.1 specs.
|
29
|
+
|
30
|
+
### Request validation
|
31
|
+
|
32
|
+
The request validation middleware returns a 4xx if the request is invalid or not defined in the API description.
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
use OpenapiFirst::Middlewares::RequestValidation, spec: 'openapi.yaml'
|
36
|
+
```
|
37
|
+
|
38
|
+
#### Options
|
39
|
+
|
40
|
+
| Name | Possible values | Description |
|
41
|
+
| :---------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
|
42
|
+
| `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` |
|
43
|
+
| `raise_error:` | `false` (default), `true` | If set to true the middleware raises `OpenapiFirst::RequestInvalidError` or `OpenapiFirst::NotFoundError` instead of returning 4xx. |
|
44
|
+
| `error_response:` | `:default` (default), `:jsonapi`, Your implementation of `ErrorResponse` |
|
45
|
+
|
46
|
+
#### Error responses
|
47
|
+
|
48
|
+
openapi_first produces a useful machine readable error response that can be customized.
|
49
|
+
The default response looks like this. See also [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457).
|
50
|
+
|
51
|
+
```json
|
52
|
+
http-status: 400
|
53
|
+
content-type: "application/problem+json"
|
54
|
+
|
55
|
+
{
|
56
|
+
"title": "Bad Request Body",
|
57
|
+
"status": 400,
|
58
|
+
"errors": [
|
59
|
+
{
|
60
|
+
"message": "value at `/data/name` is not a string",
|
61
|
+
"pointer": "/data/name",
|
62
|
+
"code": "string"
|
63
|
+
},
|
64
|
+
{
|
65
|
+
"message": "number at `/data/numberOfLegs` is less than: 2",
|
66
|
+
"pointer": "/data/numberOfLegs",
|
67
|
+
"code": "minimum"
|
68
|
+
},
|
69
|
+
{
|
70
|
+
"message": "object at `/data` is missing required properties: mandatory",
|
71
|
+
"pointer": "/data",
|
72
|
+
"code": "required"
|
73
|
+
}
|
74
|
+
]
|
75
|
+
}
|
76
|
+
```
|
77
|
+
|
78
|
+
openapi_first offers a [JSON:API](https://jsonapi.org/) error response as well:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
use OpenapiFirst::Middlewares::RequestValidation, spec: 'openapi.yaml, error_response: :jsonapi'
|
82
|
+
```
|
83
|
+
|
84
|
+
<details>
|
85
|
+
<summary>See details of JSON:API error response</summary>
|
86
|
+
|
87
|
+
```json
|
88
|
+
// http-status: 400
|
89
|
+
// content-type: "application/vnd.api+json"
|
90
|
+
|
91
|
+
{
|
92
|
+
"errors": [
|
93
|
+
{
|
94
|
+
"status": "400",
|
95
|
+
"source": {
|
96
|
+
"pointer": "/data/name"
|
97
|
+
},
|
98
|
+
"title": "value at `/data/name` is not a string",
|
99
|
+
"code": "string"
|
100
|
+
},
|
101
|
+
{
|
102
|
+
"status": "400",
|
103
|
+
"source": {
|
104
|
+
"pointer": "/data/numberOfLegs"
|
105
|
+
},
|
106
|
+
"title": "number at `/data/numberOfLegs` is less than: 2",
|
107
|
+
"code": "minimum"
|
108
|
+
},
|
109
|
+
{
|
110
|
+
"status": "400",
|
111
|
+
"source": {
|
112
|
+
"pointer": "/data"
|
113
|
+
},
|
114
|
+
"title": "object at `/data` is missing required properties: mandatory",
|
115
|
+
"code": "required"
|
116
|
+
}
|
117
|
+
]
|
118
|
+
}
|
119
|
+
```
|
120
|
+
|
121
|
+
</details>
|
122
|
+
|
123
|
+
#### Custom error responses
|
124
|
+
|
125
|
+
You can build your own custom error response with `error_response: MyCustomClass` that implements `OpenapiFirst::ErrorResponse`.
|
126
|
+
|
127
|
+
#### readOnly / writeOnly properties
|
128
|
+
|
129
|
+
Request validation fails if request includes a property with `readOnly: true`.
|
130
|
+
|
131
|
+
Response validation fails if response body includes a property with `writeOnly: true`.
|
132
|
+
|
133
|
+
### Response validation
|
134
|
+
|
135
|
+
This middleware is especially useful when testing. It _always_ raises an error if the response is not valid.
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
use OpenapiFirst::Middlewares::ResponseValidation, spec: 'openapi.yaml' if ENV['RACK_ENV'] == 'test'
|
139
|
+
```
|
140
|
+
|
141
|
+
#### Options
|
142
|
+
|
143
|
+
| Name | Possible values | Description |
|
144
|
+
| :------ | --------------- | ---------------------------------------------------------------- |
|
145
|
+
| `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` |
|
146
|
+
|
147
|
+
## Manual use
|
148
|
+
|
149
|
+
Load the API description:
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
require 'openapi_first'
|
153
|
+
|
154
|
+
definition = OpenapiFirst.load('openapi.yaml')
|
155
|
+
```
|
156
|
+
|
157
|
+
### Validate request
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
# Find and validate request
|
161
|
+
rack_request = Rack::Request.new(env)
|
162
|
+
request = definition.validate_request(rack_request)
|
163
|
+
# Or raise an exception if validation fails:
|
164
|
+
request = definition.validate_request(rack_request, raise_error: true) # Raises OpenapiFirst::RequestInvalidError or OpenapiFirst::NotFoundError if request is invalid
|
165
|
+
|
166
|
+
# Inspect the request and access parsed parameters
|
167
|
+
request.known? # Is the request defined in the API description?
|
168
|
+
request.valid? # => true / false
|
169
|
+
request.error # => Failure object if request is invalid
|
170
|
+
request.body # alias: parsed_body
|
171
|
+
request.path_parameters # => { "pet_id" => 42 }
|
172
|
+
request.query # alias: query_parameters
|
173
|
+
request.params # Merged path and query parameters
|
174
|
+
request.headers
|
175
|
+
request.cookies
|
176
|
+
request.content_type
|
177
|
+
request.request_method # => "get"
|
178
|
+
request.path # => "/pets/42"
|
179
|
+
```
|
180
|
+
|
181
|
+
### Validate response
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
# Find and validate the response
|
185
|
+
rack_response = Rack::Response[*app.call(env)]
|
186
|
+
response = definition.validate_response(rack_request, rack_response)
|
187
|
+
|
188
|
+
# Raise an exception if validation fails:
|
189
|
+
response = definition.validate_response(rack_request,rack_response, raise_error: true) # Raises OpenapiFirst::ResponseInvalidError or OpenapiFirst::ResponseNotFoundError
|
190
|
+
# Or you can also call a method on the request object mentioned above
|
191
|
+
request.validate_response(rack_response)
|
192
|
+
|
193
|
+
# Inspect the response and access parsed parameters and
|
194
|
+
response.known? # Is the response defined in the API description?
|
195
|
+
response.valid? # => true / false
|
196
|
+
response.error # => Failure object if response is invalid
|
197
|
+
response.body
|
198
|
+
request.headers
|
199
|
+
response.status # => 200
|
200
|
+
response.content_type
|
201
|
+
```
|
202
|
+
|
203
|
+
OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
|
204
|
+
|
205
|
+
## Configuration
|
206
|
+
|
207
|
+
You can configure default options globally:
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
OpenapiFirst.configure do |config|
|
211
|
+
# Specify which plugin is used to render error responses returned by the request validation middleware (defaults to :default)
|
212
|
+
config.request_validation_error_response = :jsonapi
|
213
|
+
# Configure if the request validation middleware should raise an exception (defaults to false)
|
214
|
+
config.request_validation_raise_error = true
|
215
|
+
end
|
216
|
+
```
|
217
|
+
|
218
|
+
## Framework integration
|
219
|
+
|
220
|
+
Using rack middlewares is supported in probably all Ruby web frameworks.
|
221
|
+
If you are using Ruby on Rails for example, you can add the request validation middleware globally in `config/application.rb` or inside specific controllers.
|
222
|
+
|
223
|
+
When running integration tests (or request specs when using rspec), it makes sense to add the response validation middleware to `config/environments/test.rb`:
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
config.middleware.use OpenapiFirst::Middlewares::ResponseValidation,
|
227
|
+
spec: 'api/openapi.yaml'
|
228
|
+
```
|
229
|
+
|
230
|
+
That way you don't have to call specific test assertions to make sure your API matches the OpenAPI document.
|
231
|
+
There is no need to run response validation on production if your test coverage is decent.
|
232
|
+
|
233
|
+
## Alternatives
|
234
|
+
|
235
|
+
This gem was inspired by [committe](https://github.com/interagent/committee) (Ruby) and [Connexion](https://github.com/spec-first/connexion) (Python).
|
236
|
+
Here is a [feature comparison between openapi_first and committee](https://gist.github.com/ahx/1538c31f0652f459861713b5259e366a).
|
237
|
+
|
238
|
+
## Development
|
239
|
+
|
240
|
+
Run `bin/setup` to install dependencies.
|
241
|
+
|
242
|
+
See `bundle exec rake` to run the linter and the tests.
|
243
|
+
|
244
|
+
Run `bundle exec rspec` to run the tests only.
|
245
|
+
|
246
|
+
### Benchmarks
|
247
|
+
|
248
|
+
[Results](https://gist.github.com/ahx/e6ffced58bd2e8d5baffb2f4d2c1f823)
|
249
|
+
|
250
|
+
Run benchmarks:
|
251
|
+
|
252
|
+
```sh
|
253
|
+
cd benchmarks
|
254
|
+
bundle
|
255
|
+
bundle exec ruby benchmarks.rb
|
256
|
+
```
|
257
|
+
|
258
|
+
### Contributing
|
259
|
+
|
260
|
+
If you have a question or an idea or found a bug don't hesitate to [create an issue](https://github.com/ahx/openapi_first/issues) or [start a discussion](https://github.com/ahx/openapi_first/discussions).
|
261
|
+
|
262
|
+
Pull requests are very welcome as well, of course. Feel free to create a "draft" pull request early on, even if your change is still work in progress. 🤗
|
@@ -4,7 +4,7 @@ module OpenapiFirst
|
|
4
4
|
# Global configuration. Currently only used for the request validation middleware.
|
5
5
|
class Configuration
|
6
6
|
def initialize
|
7
|
-
@request_validation_error_response = OpenapiFirst.
|
7
|
+
@request_validation_error_response = OpenapiFirst.find_plugin(:default)::ErrorResponse
|
8
8
|
@request_validation_raise_error = false
|
9
9
|
end
|
10
10
|
|
@@ -13,7 +13,7 @@ module OpenapiFirst
|
|
13
13
|
|
14
14
|
def request_validation_error_response=(mod)
|
15
15
|
@request_validation_error_response = if mod.is_a?(Symbol)
|
16
|
-
OpenapiFirst.
|
16
|
+
OpenapiFirst.find_plugin(:default)::ErrorResponse
|
17
17
|
else
|
18
18
|
mod
|
19
19
|
end
|
@@ -66,6 +66,10 @@ module OpenapiFirst
|
|
66
66
|
end
|
67
67
|
|
68
68
|
def find_response_object(status)
|
69
|
+
# According to OAS status has to be a string,
|
70
|
+
# but there are a few API descriptions out there that use integers because of YAML.
|
71
|
+
return @responses_object[status] if @responses_object.key?(status)
|
72
|
+
|
69
73
|
@responses_object[status.to_s] ||
|
70
74
|
@responses_object["#{status / 100}XX"] ||
|
71
75
|
@responses_object["#{status / 100}xx"] ||
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This is a fork of the json_refs gem, which does not use
|
4
|
+
# open-uri, does not call chdir and adds caching of files during dereferencing.
|
5
|
+
# The original code is available at https://github.com/tzmfreedom/json_refs
|
6
|
+
# See also https://github.com/tzmfreedom/json_refs/pull/11
|
7
|
+
# The code was originally written by Makoto Tajitsu with the MIT License.
|
8
|
+
#
|
9
|
+
# The MIT License (MIT)
|
10
|
+
|
11
|
+
# Copyright (c) 2017 Makoto Tajitsu
|
12
|
+
|
13
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
14
|
+
# of this software and associated documentation files (the "Software"), to deal
|
15
|
+
# in the Software without restriction, including without limitation the rights
|
16
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
17
|
+
# copies of the Software, and to permit persons to whom the Software is
|
18
|
+
# furnished to do so, subject to the following conditions:
|
19
|
+
|
20
|
+
# The above copyright notice and this permission notice shall be included in
|
21
|
+
# all copies or substantial portions of the Software.
|
22
|
+
|
23
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
24
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
25
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
26
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
27
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
28
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
29
|
+
# THE SOFTWARE.
|
30
|
+
|
31
|
+
require 'hana'
|
32
|
+
require 'json'
|
33
|
+
require 'yaml'
|
34
|
+
|
35
|
+
module JsonRefs
|
36
|
+
class << self
|
37
|
+
def dereference(doc)
|
38
|
+
file_cache = {}
|
39
|
+
Dereferencer.new(Dir.pwd, doc, file_cache).call
|
40
|
+
end
|
41
|
+
|
42
|
+
def load(filename)
|
43
|
+
doc_dir = File.dirname(filename)
|
44
|
+
doc = Loader.handle(filename)
|
45
|
+
file_cache = {}
|
46
|
+
Dereferencer.new(doc_dir, doc, file_cache).call
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
module LocalRef
|
51
|
+
module_function
|
52
|
+
|
53
|
+
def call(path:, doc:)
|
54
|
+
Hana::Pointer.new(path[1..]).eval(doc)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module Loader
|
59
|
+
module_function
|
60
|
+
|
61
|
+
def handle(filename)
|
62
|
+
body = File.read(filename)
|
63
|
+
return JSON.parse(body) if File.extname(filename) == '.json'
|
64
|
+
|
65
|
+
YAML.unsafe_load(body)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Dereferencer
|
70
|
+
def initialize(doc_dir, doc, file_cache)
|
71
|
+
@doc = doc
|
72
|
+
@doc_dir = doc_dir
|
73
|
+
@file_cache = file_cache
|
74
|
+
end
|
75
|
+
|
76
|
+
def call(doc = @doc, keys = [])
|
77
|
+
if doc.is_a?(Array)
|
78
|
+
doc.each_with_index do |value, idx|
|
79
|
+
call(value, keys + [idx])
|
80
|
+
end
|
81
|
+
elsif doc.is_a?(Hash)
|
82
|
+
if doc.key?('$ref')
|
83
|
+
dereference(keys, doc['$ref'])
|
84
|
+
else
|
85
|
+
doc.each do |key, value|
|
86
|
+
call(value, keys + [key])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
doc
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
attr_reader :doc_dir
|
96
|
+
|
97
|
+
def dereference(paths, referenced_path)
|
98
|
+
key = paths.pop
|
99
|
+
target = paths.inject(@doc) do |obj, k|
|
100
|
+
obj[k]
|
101
|
+
end
|
102
|
+
value = follow_referenced_value(referenced_path)
|
103
|
+
target[key] = value
|
104
|
+
end
|
105
|
+
|
106
|
+
def follow_referenced_value(referenced_path)
|
107
|
+
value = referenced_value(referenced_path)
|
108
|
+
return referenced_value(value['$ref']) if value.is_a?(Hash) && value.key?('$ref')
|
109
|
+
|
110
|
+
value
|
111
|
+
end
|
112
|
+
|
113
|
+
def referenced_value(referenced_path)
|
114
|
+
filepath, pointer = referenced_path.split('#')
|
115
|
+
pointer&.prepend('#')
|
116
|
+
return dereference_local(pointer) if filepath.empty?
|
117
|
+
|
118
|
+
dereferenced_file = dereference_file(filepath)
|
119
|
+
return dereferenced_file if pointer.nil?
|
120
|
+
|
121
|
+
LocalRef.call(
|
122
|
+
path: pointer,
|
123
|
+
doc: dereferenced_file
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
def dereference_local(referenced_path)
|
128
|
+
LocalRef.call(path: referenced_path, doc: @doc)
|
129
|
+
end
|
130
|
+
|
131
|
+
def dereference_file(referenced_path)
|
132
|
+
referenced_path = File.expand_path(referenced_path, doc_dir) unless File.absolute_path?(referenced_path)
|
133
|
+
@file_cache[referenced_path] ||= load_referenced_file(referenced_path)
|
134
|
+
end
|
135
|
+
|
136
|
+
def load_referenced_file(absolute_path)
|
137
|
+
directory = File.dirname(absolute_path)
|
138
|
+
|
139
|
+
referenced_doc = Loader.handle(absolute_path)
|
140
|
+
Dereferencer.new(directory, referenced_doc, @file_cache).call
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -22,6 +22,9 @@ module OpenapiFirst
|
|
22
22
|
@definition = spec.is_a?(Definition) ? spec : OpenapiFirst.load(spec)
|
23
23
|
end
|
24
24
|
|
25
|
+
# @attr_reader [Proc] app The upstream Rack application
|
26
|
+
attr_reader :app
|
27
|
+
|
25
28
|
def call(env)
|
26
29
|
request = find_request(env)
|
27
30
|
return @app.call(env) unless request
|
@@ -40,7 +43,7 @@ module OpenapiFirst
|
|
40
43
|
end
|
41
44
|
|
42
45
|
def error_response(mod)
|
43
|
-
return OpenapiFirst.
|
46
|
+
return OpenapiFirst.find_plugin(mod)::ErrorResponse if mod.is_a?(Symbol)
|
44
47
|
|
45
48
|
mod || OpenapiFirst.configuration.request_validation_error_response
|
46
49
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rack'
|
4
|
+
|
4
5
|
module OpenapiFirst
|
5
6
|
module Middlewares
|
6
7
|
# A Rack middleware to validate requests against an OpenAPI API description
|
@@ -17,6 +18,9 @@ module OpenapiFirst
|
|
17
18
|
@definition = spec.is_a?(Definition) ? spec : OpenapiFirst.load(spec)
|
18
19
|
end
|
19
20
|
|
21
|
+
# @attr_reader [Proc] app The upstream Rack application
|
22
|
+
attr_reader :app
|
23
|
+
|
20
24
|
def call(env)
|
21
25
|
request = find_request(env)
|
22
26
|
status, headers, body = @app.call(env)
|
@@ -7,13 +7,18 @@ module OpenapiFirst
|
|
7
7
|
# @!visibility private
|
8
8
|
module Plugins
|
9
9
|
PLUGINS = {} # rubocop:disable Style/MutableConstant
|
10
|
+
private_constant :PLUGINS
|
10
11
|
|
11
12
|
def register(name, klass)
|
12
|
-
PLUGINS[name] = klass
|
13
|
+
PLUGINS[name.to_sym] = klass
|
13
14
|
end
|
14
15
|
|
15
16
|
def plugin(name)
|
16
17
|
require "openapi_first/plugins/#{name}"
|
18
|
+
PLUGINS.fetch(name.to_sym)
|
19
|
+
end
|
20
|
+
|
21
|
+
def find_plugin(name)
|
17
22
|
PLUGINS.fetch(name)
|
18
23
|
end
|
19
24
|
end
|
data/lib/openapi_first.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'yaml'
|
4
4
|
require 'multi_json'
|
5
|
-
|
5
|
+
require_relative 'openapi_first/json_refs'
|
6
6
|
require_relative 'openapi_first/errors'
|
7
7
|
require_relative 'openapi_first/configuration'
|
8
8
|
require_relative 'openapi_first/plugins'
|
@@ -35,7 +35,7 @@ module OpenapiFirst
|
|
35
35
|
# Load and dereference an OpenAPI spec file
|
36
36
|
# @return [Definition]
|
37
37
|
def self.load(filepath, only: nil)
|
38
|
-
resolved =
|
38
|
+
resolved = Bundle.resolve(filepath)
|
39
39
|
parse(resolved, only:, filepath:)
|
40
40
|
end
|
41
41
|
|
@@ -46,24 +46,13 @@ module OpenapiFirst
|
|
46
46
|
Definition.new(resolved, filepath)
|
47
47
|
end
|
48
48
|
|
49
|
-
# @!visibility private
|
50
|
-
def self.bundle(filepath)
|
51
|
-
Bundle.resolve(filepath)
|
52
|
-
end
|
53
|
-
|
54
49
|
# @!visibility private
|
55
50
|
module Bundle
|
56
51
|
def self.resolve(spec_path)
|
57
|
-
|
58
|
-
content = load_file(File.basename(spec_path))
|
59
|
-
JsonRefs.call(content, resolve_local_ref: true, resolve_file_ref: true)
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def self.load_file(spec_path)
|
64
|
-
return MultiJson.load(File.read(spec_path)) if File.extname(spec_path) == '.json'
|
65
|
-
|
66
|
-
YAML.unsafe_load_file(spec_path)
|
52
|
+
JsonRefs.load(spec_path)
|
67
53
|
end
|
68
54
|
end
|
69
55
|
end
|
56
|
+
|
57
|
+
OpenapiFirst.plugin(:default)
|
58
|
+
OpenapiFirst.plugin(:jsonapi)
|
metadata
CHANGED
@@ -1,35 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: openapi_first
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.3.
|
4
|
+
version: 1.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andreas Haller
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-02-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: hana
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
20
|
-
- - ">="
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: 0.1.7
|
19
|
+
version: '1.3'
|
23
20
|
type: :runtime
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
26
23
|
requirements:
|
27
24
|
- - "~>"
|
28
25
|
- !ruby/object:Gem::Version
|
29
|
-
version: '
|
30
|
-
- - ">="
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: 0.1.7
|
26
|
+
version: '1.3'
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
28
|
name: json_schemer
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -119,6 +113,9 @@ executables: []
|
|
119
113
|
extensions: []
|
120
114
|
extra_rdoc_files: []
|
121
115
|
files:
|
116
|
+
- CHANGELOG.md
|
117
|
+
- LICENSE.txt
|
118
|
+
- README.md
|
122
119
|
- lib/openapi_first.rb
|
123
120
|
- lib/openapi_first/body_parser.rb
|
124
121
|
- lib/openapi_first/configuration.rb
|
@@ -131,6 +128,7 @@ files:
|
|
131
128
|
- lib/openapi_first/error_response.rb
|
132
129
|
- lib/openapi_first/errors.rb
|
133
130
|
- lib/openapi_first/failure.rb
|
131
|
+
- lib/openapi_first/json_refs.rb
|
134
132
|
- lib/openapi_first/middlewares/request_validation.rb
|
135
133
|
- lib/openapi_first/middlewares/response_validation.rb
|
136
134
|
- lib/openapi_first/plugins.rb
|
@@ -152,6 +150,7 @@ licenses:
|
|
152
150
|
- MIT
|
153
151
|
metadata:
|
154
152
|
homepage_uri: https://github.com/ahx/openapi_first
|
153
|
+
documentation_uri: https://www.rubydoc.info/gems/openapi_first/
|
155
154
|
source_code_uri: https://github.com/ahx/openapi_first
|
156
155
|
changelog_uri: https://github.com/ahx/openapi_first/blob/main/CHANGELOG.md
|
157
156
|
rubygems_mfa_required: 'true'
|