openapi_first 1.3.1 → 1.3.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7e9e0bad95211ba2b4f103089abc40d0e7fc46a24f7ce8e57318ba489655637
4
- data.tar.gz: f1d28fbaa79a4eb7e00b242eb9bd89a78789bbb04742e0b87501ac454fb1cf47
3
+ metadata.gz: 5505b1339baa8f9dcfdcbedb2270eb67436f0661ebd04115bc6d3346da9d09c6
4
+ data.tar.gz: 4bea9a7977b95fbb05499d2c1a01eaef54dbabcdccab85c0ab327059e4450db4
5
5
  SHA512:
6
- metadata.gz: f225e3a49d7f582e0f82822b2c9acbcd6008b6cc2464971e1485ef75ec12e508effc2165b6afc91535e27bc9bb9004f92fb160218f7295b0f45c3f58946ed616
7
- data.tar.gz: 4c6ef209df4816361bed148f33b4e55ace50a5481172913c5760d014b09247adf74ffdbd2422ae930cd889494fe941c20d492835863d52a9dde67a75c1bd8622
6
+ metadata.gz: 78597035bfd86554fe437f8c26d285572bdc7eb5c19a8e7fa70cb55942e3df3f57bb7851666787b111fbf29e59ac03eb1a1db2bbf8d81b1f92d50298c3559e74
7
+ data.tar.gz: ff9b1d5871a855187aef2de7c6eec5324b8ba32c95560d1478f5b61566f7d53d7138d5a52dc4a13a78c5a518f6de83fec6f56ef2354509297cfcd15202cc3eda
data/CHANGELOG.md ADDED
@@ -0,0 +1,317 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 1.3.4
6
+
7
+ - Fixed handling "binary" format in optional multipart file uploads
8
+ - Cache the resolved OAD. This especially makes things run faster in tests.
9
+
10
+ ## 1.3.3 (yanked)
11
+
12
+ ## 1.3.2
13
+
14
+ ### Changed
15
+
16
+ - 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.
17
+
18
+ ### Fixed
19
+
20
+ - Reduced initial load time for composed API descriptions [#232](https://github.com/ahx/openapi_first/pull/232)
21
+ - Chore: Add Readme back to gem. Add link to docs.
22
+
23
+ ## 1.3.1
24
+
25
+ - Fixed warning about duplicated constant
26
+
27
+ ## 1.3.0
28
+
29
+ No breaking changes
30
+
31
+ New features:
32
+
33
+ - Added new API: `Definition#validate_request`, `Definition#validate_response`, `RuntimeRequest#validate_response` (see readme) [#222](https://github.com/ahx/openapi_first/pull/222)
34
+
35
+ Fixes:
36
+
37
+ - Manual response validation (without the middleware) just works in Rails' request tests now. [#224](https://github.com/ahx/openapi_first/pull/224)
38
+
39
+ ## 1.2.0
40
+
41
+ No breaking changes
42
+
43
+ - Added `OpenapiFirst.parse(hash)` to load ("parse") a resolved/de-referenced Hash
44
+ - Added support for unescaped special characters in the path params (https://github.com/ahx/openapi_first/pull/217)
45
+ - Added `operation` to `RuntimeRequest` by [@MrBananaLord](https://github.com/ahx/openapi_first/pull/216)
46
+
47
+ ## 1.1.1
48
+
49
+ - Fix reading response body for example when running Rails (`ActionDispatch::Response::RackBody`)
50
+ - Add `known?`, `status`, `body`, `headers`, `content_type` methods to inspect the parsed response (`RuntimeResponse`)
51
+ - Add `OpenapiFirst::ParseError` which is raised by low-level interfaces like `request.body` if the body could not be parsed.
52
+ - Add "code" field to errors in JSON:API error response
53
+
54
+ ## 1.1.0 (yanked)
55
+
56
+ ## 1.0.0
57
+
58
+ - Breaking: The default error uses application/problem+json content-type
59
+ - Breaking: Moved rack middlewares to OpenapiFirst::Middlewares
60
+ - Breaking: Rename OpenapiFirst::ResponseInvalid to OpenapiFirst::ResponseInvalidError
61
+ - Breaking: Remove OpenapiFirst::Router
62
+ - Breaking: Remove `env[OpenapiFirst::OPERATION]`. Use `env[OpenapiFirst::REQUEST]` instead.
63
+ - Breaking: Remove `env[OpenapiFirst::REQUEST_BODY]`, `env[OpenapiFirst::PARAMS]`. Use `env[OpenapiFirst::REQUEST].body env[OpenapiFirst::REQUEST].params` instead.
64
+ - Add interface to validate requests / responses without middlewares (see "Manual validation" in README)
65
+ - Add OpenapiFirst.configure
66
+ - Add OpenapiFirst.register, OpenapiFirst.plugin
67
+ - Fix response header validation with Rack 3
68
+ - Fixed: Add support for paths like `/{a}..{b}`
69
+
70
+ ## 1.0.0.beta6
71
+
72
+ - Fix: Make response header validation work with rack 3
73
+ - Refactor router
74
+ - Remove dependency hanami-router
75
+ - PathItem and Operation for a request can be found by calling methods on the Definitnion
76
+ - Fixed https://github.com/ahx/openapi_first/issues/155
77
+ - Breaking / Regression: A paths like /pets/{from}-{to} if there is a path "/pets/{id}"
78
+
79
+ ## 1.0.0.beta5
80
+
81
+ - Added: `OpenapiFirst::Config.default_options=` to set default options globally
82
+ - Added: You can define custom error responses by subclassing `OpenapiFirst::ErrorResponse` and register it via `OpenapiFirst.register_error_response(name, MyCustomErrorResponse)`
83
+
84
+ ## 1.0.0.beta4
85
+
86
+ - Update json_schemer to version 2.0
87
+ - Breaking: Requires Ruby 3.1 or later
88
+ - 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]`
89
+ - Breaking / Added: ResponseValidation now validates response headers
90
+ - Breaking / Added: RequestValidation now validates cookie, path and header parameters
91
+ - Breaking: multipart File uploads are now read and then validated
92
+ - Breaking: Remove OpenapiFirst.env method
93
+ - Breaking: Request validation returns 400 instead of 415 if request body is required, but empty
94
+
95
+ ## 1.0.0.beta3
96
+
97
+ - Remove obsolete dependency: deep_merge
98
+ - Remove obsolete dependency: hanami-utils
99
+
100
+ ## 1.0.0.beta2
101
+
102
+ - Fixed dependencies. Remove unused code.
103
+
104
+ ## 1.0.0.beta1
105
+
106
+ - Removed: `OpenapiFirst::Responder` and `OpenapiFirst::RackResponder`
107
+ - Removed: `OpenapiFirst.app` and `OpenapiFirst.middleware`
108
+ - Removed: `OpenapiFirst::Coverage`
109
+ - Breaking: Parsed query and path parameters are available at `env[OpenapiFirst::PARAMS]`(or `env['openapi.params']`) instead of `OpenapiFirst::PARAMETERS`.
110
+ - Breaking: Request body and parameters now use string keys instead of symbols!
111
+ - Breaking: Query parameters are now parsed exactly like in the API description via the openapi_parameters gem. This means a couple of things:
112
+ - Query parameters now support `explode: true` (default) and `explode: false` for array and object parameters.
113
+ - Query parameters with brackets like 'filter[tag]' are no longer deconstructed into nested hashes, but accessible via the `filter[tag]` key.
114
+ - 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.
115
+ - Path parameters are now parsed exactly as in the API description via the openapi_parameters gem.
116
+
117
+ ## 0.21.0
118
+
119
+ - Fix: Query parameter validation does not fail if header parameters are defined (Thanks to [JF Lalonde](https://github.com/JF-Lalonde))
120
+ - Update Ruby dependency to >= 3.0.5
121
+ - Handle simple form-data in request bodies (see https://github.com/ahx/openapi_first/issues/149)
122
+ - Update to hanami-router 2.0.0 stable
123
+
124
+ ## 0.20.0
125
+
126
+ - You can pass a filepath to `spec:` now so you no longer have to call `OpenapiFirst.load` anymore.
127
+ - Router is optional now.
128
+ 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.
129
+ If you don't add the Router, make sure you pass `spec:` to your request/response validation middleware.
130
+ - Support "4xx" and "4XX" response definitions.
131
+ (4XX is defined in the standard, but 2xx is used in the wild as well 🦁.)
132
+ - Removed warning about missing operationId, because operationId is not used until the Responder is used.
133
+ - Raise HandlerNotFoundError when handler cannot be found
134
+
135
+ ## 0.19.0
136
+
137
+ - Add `RackResponder`
138
+
139
+ - BREAKING CHANGE: Handler classes are now instantiated only once without any arguments and the same instance is called on each following call/request.
140
+
141
+ ## 0.18.0
142
+
143
+ Yanked. No useful changes.
144
+
145
+ ## 0.17.0
146
+
147
+ - BREAKING CHANGE: Use a Hash instead of named arguments for middleware options for better compatibility
148
+ Using named arguments is actually not supported in Rack.
149
+
150
+ ## 0.16.1
151
+
152
+ - Pin hanami-router version, because alpha6 is broken.
153
+
154
+ ## 0.16.0
155
+
156
+ - Support status code wildcards like "2XX", "4XX"
157
+
158
+ ## 0.15.0
159
+
160
+ - Populate default parameter values
161
+
162
+ ## 0.14.3
163
+
164
+ - Use json_refs to resolve OpenAPI file. This removes oas_parser and ActiveSupport from list of dependencies
165
+
166
+ ## 0.14.2
167
+
168
+ - 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.
169
+
170
+ ## 0.14.1
171
+
172
+ - Fix: Don't mix path- and operation-level parameters for request validation
173
+
174
+ ## 0.14.0
175
+
176
+ - Handle custom x-handler field in the API description to find a handler method not based on operationId
177
+ - Add `resolver` option to provide a custom resolver to find a handler method
178
+
179
+ ## 0.13.3
180
+
181
+ - Better error message if string does not match format
182
+ - readOnly and writeOnly just works when used inside allOf
183
+
184
+ ## 0.13.2
185
+
186
+ - Return indicator (`source: { parameter: 'list/1' }`) in error response body when array item in query parameter is invalid
187
+
188
+ ## 0.13.0
189
+
190
+ - Add support for arrays in query parameters (style: form, explode: false)
191
+ - Remove warning when handler is not implemented
192
+
193
+ ## 0.12.5
194
+
195
+ - Add `not_found: :continue` option to Router to make it do nothing if request is unknown
196
+
197
+ ## 0.12.4
198
+
199
+ - content-type is found while ignoring additional content-type parameters (`application/json` is found when request/response content-type is `application/json; charset=UTF8`)
200
+ - Support wildcard mime-types when finding the content-type
201
+
202
+ ## 0.12.3
203
+
204
+ - Add `response_validation:`, `router_raise_error` options to standalone mode.
205
+
206
+ ## 0.12.2
207
+
208
+ - Allow response to have no media type object specified
209
+
210
+ ## 0.12.1
211
+
212
+ - Fix response when handler returns 404 or 405
213
+ - Don't validate the response content if status is 204 (no content)
214
+
215
+ ## 0.12.0
216
+
217
+ - Change `ResponseValidator` to raise an exception if it found a problem
218
+ - Params have symbolized keys now
219
+ - Remove `not_found` option from Router. Return 405 if HTTP verb is not allowed (via Hanami::Router)
220
+ - Add `raise_error` option to OpenapiFirst.app (false by default)
221
+ - Add ResponseValidation to OpenapiFirst.app if raise_error option is true
222
+ - Rename `raise` option to `raise_error`
223
+ - Add `raise_error` option to RequestValidation middleware
224
+ - Raise error if handler could not be found by Responder
225
+ - Add `Operation#name` that returns a human readable name for an operation
226
+
227
+ ## 0.11.0
228
+
229
+ - Raise error if you forgot to add the Router middleware
230
+ - Make OpenapiFirst.app raise an error in test env when request path is not specified
231
+ - Rename OperationResolver to Responder
232
+ - Add ResponseValidation middleware that validates the response body
233
+ - 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.
234
+ - Move namespace option from Router to OperationResolver
235
+
236
+ ## 0.10.2
237
+
238
+ - Return 400 if request body has invalid JSON ([issue](https://github.com/ahx/openapi_first/issues/73)) thanks Thomas Frütel
239
+
240
+ ## 0.10.1
241
+
242
+ - Fix duplicated key in `required` when generating JSON schema for `some[thing]` parameters
243
+
244
+ ## 0.10.0
245
+
246
+ - Add support for query parameters named `"some[thing]"` ([issue](https://github.com/ahx/openapi_first/issues/40))
247
+
248
+ ## 0.9.0
249
+
250
+ - Make request validation usable standalone
251
+
252
+ ## 0.8.0
253
+
254
+ - Add merged parameter and request body available to env at `env[OpenapiFirst::INBOX]` in request validation
255
+ - Path and query parameters with `type: boolean` now get converted to `true`/`false`
256
+ - Rename `OpenapiFirst::PARAMS` to `OpenapiFirst::PARAMETERS`
257
+
258
+ ## 0.7.1
259
+
260
+ - Add missing `require` to work with new version of `oas_parser`
261
+
262
+ ## 0.7.0
263
+
264
+ - Make use of hanami-router, because it's fast
265
+ - Remove option `allow_unknown_query_paramerters`
266
+ - Move the namespace option to Router
267
+ - Convert numeric path and query parameters to `Integer` or `Float`
268
+ - Pass the Rack env if your action class' initializers accepts an argument
269
+ - Respec rack's `env['SCRIPT_NAME']` in router
270
+ - Add MIT license
271
+
272
+ ## 0.6.10
273
+
274
+ - Bugfix: params.env['unknown'] now returns `nil` as expected. Thanks @tristandruyen.
275
+
276
+ ## 0.6.9
277
+
278
+ - Removed radix tree, because of a bug (https://github.com/namusyaka/r2ree-ruby/issues/2)
279
+
280
+ ## 0.6.8
281
+
282
+ - Performance: About 25% performance increase (i/s) with help of c++ based radix-tree and some optimizations
283
+ - Update dependencies
284
+
285
+ ## 0.6.7
286
+
287
+ - Fix: version number of oas_parser
288
+
289
+ ## 0.6.6
290
+
291
+ - Remove warnings for Ruby 2.7
292
+
293
+ ## 0.6.5
294
+
295
+ - Merge QueryParameterValidation and ReqestBodyValidation middlewares into RequestValidation
296
+ - Rename option to `allow_unknown_query_paramerters`
297
+
298
+ ## 0.6.4
299
+
300
+ - Fix: Rewind request body after reading
301
+
302
+ ## 0.6.3
303
+
304
+ - Add option to parse only certain paths from OAS file
305
+
306
+ ## 0.6.2
307
+
308
+ - Add support to map operationIds like `things#index` or `web.things_index`
309
+
310
+ ## 0.6.1
311
+
312
+ - Make ResponseValidator errors easier to read
313
+
314
+ ## 0.6.0
315
+
316
+ - Set the content-type based on the OpenAPI description [#29](https://github.com/ahx/openapi-first/pull/29)
317
+ - 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.plugin(:default)::ErrorResponse
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.plugin(:default)::ErrorResponse
16
+ OpenapiFirst.find_plugin(:default)::ErrorResponse
17
17
  else
18
18
  mod
19
19
  end
@@ -95,7 +95,7 @@ module OpenapiFirst
95
95
  # Returns a unique name for this operation. Used for generating error messages.
96
96
  # @visibility private
97
97
  def name
98
- @name ||= "#{method.upcase} #{path} (#{operation_id})"
98
+ @name ||= "#{method.upcase} #{path}"
99
99
  end
100
100
 
101
101
  # Returns the path parameters of the operation.
@@ -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"] ||
@@ -71,8 +71,8 @@ module OpenapiFirst
71
71
  private
72
72
 
73
73
  def generate_message
74
- messages = errors&.take(4)&.map(&:error)
75
- messages << "... (#{errors.size} errors total)" if errors && errors.size > 4
74
+ messages = errors&.take(3)&.map(&:error)
75
+ messages << "... (#{errors.size} errors total)" if errors && errors.size > 3
76
76
  messages&.join('. ')
77
77
  end
78
78
  end
@@ -0,0 +1,153 @@
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 OpenapiFirst
36
+ class FileNotFoundError < StandardError; end
37
+
38
+ module JsonRefs
39
+ class << self
40
+ def dereference(doc)
41
+ file_cache = {}
42
+ Dereferencer.new(Dir.pwd, doc, file_cache).call
43
+ end
44
+
45
+ def load(filename)
46
+ doc_dir = File.dirname(filename)
47
+ doc = Loader.handle(filename)
48
+ file_cache = {}
49
+ Dereferencer.new(filename, doc_dir, doc, file_cache).call
50
+ end
51
+ end
52
+
53
+ module LocalRef
54
+ module_function
55
+
56
+ def call(path:, doc:)
57
+ Hana::Pointer.new(path[1..]).eval(doc)
58
+ end
59
+ end
60
+
61
+ module Loader
62
+ module_function
63
+
64
+ def handle(filename)
65
+ body = File.read(filename)
66
+ return JSON.parse(body) if File.extname(filename) == '.json'
67
+
68
+ YAML.unsafe_load(body)
69
+ end
70
+ end
71
+
72
+ class Dereferencer
73
+ def initialize(filename, doc_dir, doc, file_cache)
74
+ @filename = filename
75
+ @doc = doc
76
+ @doc_dir = doc_dir
77
+ @file_cache = file_cache
78
+ end
79
+
80
+ def call(doc = @doc, keys = [])
81
+ if doc.is_a?(Array)
82
+ doc.each_with_index do |value, idx|
83
+ call(value, keys + [idx])
84
+ end
85
+ elsif doc.is_a?(Hash)
86
+ if doc.key?('$ref')
87
+ dereference(keys, doc['$ref'])
88
+ else
89
+ doc.each do |key, value|
90
+ call(value, keys + [key])
91
+ end
92
+ end
93
+ end
94
+ doc
95
+ end
96
+
97
+ private
98
+
99
+ attr_reader :doc_dir
100
+
101
+ def dereference(paths, referenced_path)
102
+ key = paths.pop
103
+ target = paths.inject(@doc) do |obj, k|
104
+ obj[k]
105
+ end
106
+ value = follow_referenced_value(referenced_path)
107
+ target[key] = value
108
+ end
109
+
110
+ def follow_referenced_value(referenced_path)
111
+ value = referenced_value(referenced_path)
112
+ return referenced_value(value['$ref']) if value.is_a?(Hash) && value.key?('$ref')
113
+
114
+ value
115
+ end
116
+
117
+ def referenced_value(referenced_path)
118
+ filepath, pointer = referenced_path.split('#')
119
+ pointer&.prepend('#')
120
+ return dereference_local(pointer) if filepath.empty?
121
+
122
+ dereferenced_file = dereference_file(filepath)
123
+ return dereferenced_file if pointer.nil?
124
+
125
+ LocalRef.call(
126
+ path: pointer,
127
+ doc: dereferenced_file
128
+ )
129
+ end
130
+
131
+ def dereference_local(referenced_path)
132
+ LocalRef.call(path: referenced_path, doc: @doc)
133
+ end
134
+
135
+ def dereference_file(referenced_path)
136
+ referenced_path = File.expand_path(referenced_path, doc_dir) unless File.absolute_path?(referenced_path)
137
+ @file_cache[referenced_path] ||= load_referenced_file(referenced_path)
138
+ end
139
+
140
+ def load_referenced_file(absolute_path)
141
+ directory = File.dirname(absolute_path)
142
+
143
+ unless File.exist?(absolute_path)
144
+ raise FileNotFoundError,
145
+ "Problem while loading file referenced in #{@filename}: File not found #{absolute_path}"
146
+ end
147
+
148
+ referenced_doc = Loader.handle(absolute_path)
149
+ Dereferencer.new(@filename, directory, referenced_doc, @file_cache).call
150
+ end
151
+ end
152
+ end
153
+ 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.plugin(mod)::ErrorResponse if mod.is_a?(Symbol)
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
@@ -22,8 +22,10 @@ module OpenapiFirst
22
22
  # @attr_reader [String] content_type The content_type of the Rack::Response.
23
23
  def_delegators :@rack_response, :status, :content_type
24
24
 
25
- # @attr_reader [String] name The name of the operation. Used for generating error messages.
26
- def_delegators :@operation, :name # @visibility private
25
+ # @return [String] name The name of the operation. Used for generating error messages.
26
+ def name
27
+ "#{@operation.name} response status: #{status}"
28
+ end
27
29
 
28
30
  # Checks if the response is valid. Runs the validation unless it has been run before.
29
31
  # @return [Boolean]
@@ -42,7 +42,7 @@ module OpenapiFirst
42
42
  def binary_format(data, property, property_schema, _parent)
43
43
  return unless property_schema.is_a?(Hash) && property_schema['format'] == 'binary'
44
44
 
45
- data[property] = data[property][:tempfile].read
45
+ data[property] = data.dig(property, :tempfile)&.read if data[property]
46
46
  end
47
47
  end
48
48
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '1.3.1'
4
+ VERSION = '1.3.4'
5
5
  end
data/lib/openapi_first.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'yaml'
4
4
  require 'multi_json'
5
- require 'json_refs'
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 = bundle(filepath)
38
+ resolved = Bundle.resolve(filepath)
39
39
  parse(resolved, only:, filepath:)
40
40
  end
41
41
 
@@ -46,24 +46,14 @@ 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
- Dir.chdir(File.dirname(spec_path)) do
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
+ @file_cache ||= {}
53
+ @file_cache[File.expand_path(spec_path).to_sym] ||= JsonRefs.load(spec_path)
67
54
  end
68
55
  end
69
56
  end
57
+
58
+ OpenapiFirst.plugin(:default)
59
+ 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.1
4
+ version: 1.3.4
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-01-31 00:00:00.000000000 Z
11
+ date: 2024-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: json_refs
14
+ name: hana
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.1'
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: '0.1'
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'