openapi_first 0.7.0.alpha1 → 3.1.0.beta1
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 +585 -2
- data/LICENSE.txt +21 -0
- data/README.md +360 -114
- data/lib/openapi_first/builder.rb +214 -0
- data/lib/openapi_first/child_configuration.rb +26 -0
- data/lib/openapi_first/configuration.rb +76 -0
- data/lib/openapi_first/definition.rb +98 -13
- data/lib/openapi_first/error_response.rb +40 -0
- data/lib/openapi_first/error_responses/default.rb +74 -0
- data/lib/openapi_first/error_responses/jsonapi.rb +60 -0
- data/lib/openapi_first/errors.rb +40 -0
- data/lib/openapi_first/failure.rb +77 -0
- data/lib/openapi_first/file_loader.rb +22 -0
- data/lib/openapi_first/header.rb +9 -0
- data/lib/openapi_first/json.rb +25 -0
- data/lib/openapi_first/middlewares/request_validation.rb +58 -0
- data/lib/openapi_first/middlewares/response_validation.rb +41 -0
- data/lib/openapi_first/parsed_request.rb +5 -0
- data/lib/openapi_first/ref_resolver.rb +193 -0
- data/lib/openapi_first/registry.rb +44 -0
- data/lib/openapi_first/request.rb +79 -0
- data/lib/openapi_first/request_body_parsers.rb +64 -0
- data/lib/openapi_first/request_validator.rb +32 -0
- data/lib/openapi_first/response.rb +43 -0
- data/lib/openapi_first/response_body_parsers.rb +31 -0
- data/lib/openapi_first/response_parser.rb +50 -0
- data/lib/openapi_first/response_validator.rb +16 -48
- data/lib/openapi_first/router/find_content.rb +17 -0
- data/lib/openapi_first/router/find_response.rb +53 -0
- data/lib/openapi_first/router/path_template.rb +48 -0
- data/lib/openapi_first/router.rb +84 -57
- data/lib/openapi_first/schema/hash.rb +39 -0
- data/lib/openapi_first/schema/validation_error.rb +45 -0
- data/lib/openapi_first/schema/validation_result.rb +30 -0
- data/lib/openapi_first/test/app.rb +30 -0
- data/lib/openapi_first/test/callable.rb +25 -0
- data/lib/openapi_first/test/configuration.rb +76 -0
- data/lib/openapi_first/test/coverage/covered_request.rb +11 -0
- data/lib/openapi_first/test/coverage/covered_response.rb +11 -0
- data/lib/openapi_first/test/coverage/plan.rb +77 -0
- data/lib/openapi_first/test/coverage/request_task.rb +42 -0
- data/lib/openapi_first/test/coverage/response_task.rb +42 -0
- data/lib/openapi_first/test/coverage/route_task.rb +13 -0
- data/lib/openapi_first/test/coverage/terminal_formatter.rb +115 -0
- data/lib/openapi_first/test/coverage/tracker.rb +32 -0
- data/lib/openapi_first/test/coverage.rb +102 -0
- data/lib/openapi_first/test/methods.rb +62 -0
- data/lib/openapi_first/test/minitest_helpers.rb +28 -0
- data/lib/openapi_first/test/observe.rb +37 -0
- data/lib/openapi_first/test/observer_middleware.rb +23 -0
- data/lib/openapi_first/test/plain_helpers.rb +31 -0
- data/lib/openapi_first/test.rb +171 -0
- data/lib/openapi_first/validated_request.rb +79 -0
- data/lib/openapi_first/validated_response.rb +53 -0
- data/lib/openapi_first/validators/request_body.rb +25 -0
- data/lib/openapi_first/validators/request_parameters.rb +49 -0
- data/lib/openapi_first/validators/response_body.rb +27 -0
- data/lib/openapi_first/validators/response_headers.rb +40 -0
- data/lib/openapi_first/version.rb +1 -1
- data/lib/openapi_first.rb +67 -35
- metadata +99 -133
- data/.github/CODEOWNERS +0 -1
- data/.gitignore +0 -11
- data/.rspec +0 -3
- data/.rubocop.yml +0 -14
- data/.travis.yml +0 -8
- data/Gemfile +0 -11
- data/Gemfile.lock +0 -126
- data/Rakefile +0 -15
- data/benchmarks/Gemfile +0 -13
- data/benchmarks/Gemfile.lock +0 -144
- data/benchmarks/apps/committee.ru +0 -30
- data/benchmarks/apps/grape.ru +0 -21
- data/benchmarks/apps/hanami_router.ru +0 -14
- data/benchmarks/apps/openapi.yaml +0 -86
- data/benchmarks/apps/openapi_first.ru +0 -22
- data/benchmarks/apps/openapi_first_resolve_only.ru +0 -23
- data/benchmarks/apps/sinatra.ru +0 -26
- data/benchmarks/apps/syro.ru +0 -25
- data/benchmarks/benchmarks.rb +0 -46
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/examples/README.md +0 -13
- data/examples/app.rb +0 -12
- data/examples/config.ru +0 -7
- data/examples/openapi.yaml +0 -29
- data/lib/openapi_first/app.rb +0 -27
- data/lib/openapi_first/coverage.rb +0 -28
- data/lib/openapi_first/operation.rb +0 -48
- data/lib/openapi_first/operation_resolver.rb +0 -43
- data/lib/openapi_first/request_validation.rb +0 -144
- data/lib/openapi_first/utils.rb +0 -31
- data/lib/openapi_first/validation.rb +0 -15
- data/lib/openapi_first/validation_format.rb +0 -31
- data/openapi_first.gemspec +0 -46
data/README.md
CHANGED
|
@@ -1,204 +1,450 @@
|
|
|
1
|
-
#
|
|
1
|
+
# openapi_first
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
openapi_first is a Ruby gem for request / response validation and contract-testing against an [OpenAPI](https://www.openapis.org/) 3.0 or 3.1 Openapi API description (OAD). It makes an APIFirst workflow easy and reliable.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Usage
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
Configure
|
|
8
|
+
```ruby
|
|
9
|
+
OpenapiFirst.register('openapi/openapi.yaml')
|
|
10
|
+
```
|
|
11
11
|
|
|
12
|
+
Use an OAD to validate incoming requests:
|
|
12
13
|
```ruby
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{
|
|
16
|
-
id: params['id'],
|
|
17
|
-
name: 'Oscar'
|
|
18
|
-
}
|
|
19
|
-
end
|
|
20
|
-
end
|
|
14
|
+
use OpenapiFirst::Middlewares::RequestValidation
|
|
15
|
+
```
|
|
21
16
|
|
|
22
|
-
#
|
|
17
|
+
Turn your request tests into [contract tests](#contract-testing) against an OAD:
|
|
18
|
+
```ruby
|
|
19
|
+
# spec_helper.rb
|
|
23
20
|
require 'openapi_first'
|
|
24
|
-
|
|
25
|
-
```
|
|
21
|
+
OpenapiFirst::Test.setup
|
|
26
22
|
|
|
27
|
-
|
|
23
|
+
require 'my_app'
|
|
24
|
+
RSpec.configure do |config|
|
|
25
|
+
config.include OpenapiFirst::Test::Methods[MyApp], type: :request
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
- Map the request to a method call `Pets.find_pet` based on the `operationId` in the API description
|
|
31
|
-
- Set the response content type according to your spec (here with the default status code `200`)
|
|
29
|
+
## Contents
|
|
32
30
|
|
|
33
|
-
|
|
31
|
+
<!-- TOC -->
|
|
34
32
|
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
|
|
33
|
+
- [Configuration](#configuration)
|
|
34
|
+
- [Rack Middlewares](#rack-middlewares)
|
|
35
|
+
- [Request validation](#request-validation)
|
|
36
|
+
- [Response validation](#response-validation)
|
|
37
|
+
- [Contract testing](#contract-testing)
|
|
38
|
+
- [Test assertions](#test-assertions)
|
|
39
|
+
- [Manual use](#manual-use)
|
|
40
|
+
- [Framework integration](#framework-integration)
|
|
41
|
+
- [Hooks](#hooks)
|
|
42
|
+
- [Alternatives](#alternatives)
|
|
43
|
+
- [Frequently Asked Questions](#frequently-asked-questions)
|
|
44
|
+
- [Development](#development)
|
|
45
|
+
- [Benchmarks](#benchmarks)
|
|
46
|
+
- [Contributing](#contributing)
|
|
38
47
|
|
|
39
|
-
|
|
48
|
+
<!-- /TOC -->
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
## Configuration
|
|
42
51
|
|
|
52
|
+
You should register OADs globally so you don't have to load the file multiple times or to refernce them by Symbol (like :v1 in this example).
|
|
43
53
|
```ruby
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
OpenapiFirst.configure do |config|
|
|
55
|
+
config.register('openapi/openapi.yaml') # :default
|
|
56
|
+
config.register('openapi/v1.openapi.yaml', as: :v1)
|
|
57
|
+
end
|
|
46
58
|
```
|
|
47
59
|
|
|
48
|
-
|
|
60
|
+
You can configure default options globally:
|
|
49
61
|
|
|
50
62
|
```ruby
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
|
|
63
|
+
OpenapiFirst.configure do |config|
|
|
64
|
+
# Specify which plugin is used to render error responses returned by the request validation middleware (defaults to :default)
|
|
65
|
+
config.request_validation_error_response = :jsonapi
|
|
66
|
+
end
|
|
54
67
|
```
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
### Try it out
|
|
59
|
-
|
|
60
|
-
See [examples](examples).
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
## Installation
|
|
64
|
-
|
|
65
|
-
Add this line to your application's Gemfile:
|
|
69
|
+
or configure per instance:
|
|
66
70
|
|
|
67
71
|
```ruby
|
|
68
|
-
|
|
72
|
+
OpenapiFirst.load('openapi.yaml') do |config|
|
|
73
|
+
config.request_validation_error_response = :jsonapi
|
|
74
|
+
end
|
|
69
75
|
```
|
|
70
76
|
|
|
71
|
-
|
|
77
|
+
## Rack Middlewares
|
|
72
78
|
|
|
73
|
-
|
|
79
|
+
### Request validation
|
|
74
80
|
|
|
75
|
-
|
|
81
|
+
The request validation middleware returns a 4xx if the request is invalid or not defined in the API description. It adds a request object to the current Rack environment at `env[OpenapiFirst::REQUEST]` with the request parameters parsed exactly as described in your API description plus access to meta information from your API description. See _[Manual use](#manual-use)_ for more details about that object.
|
|
76
82
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
- "create_pet" or "createPet" or "create pet" calls `MyApi.create_pet(params, response)`
|
|
80
|
-
- "some_things.create" calls: `MyApi::SomeThings.create(params, response)`
|
|
81
|
-
- "pets#create" calls: `MyApi::Pets::Create.new.call(params, response)`
|
|
82
|
-
|
|
83
|
-
These handler methods are called with two arguments:
|
|
83
|
+
```ruby
|
|
84
|
+
use OpenapiFirst::Middlewares::RequestValidation
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
# Pass `raise_error: true` to raise an error if request is invalid:
|
|
87
|
+
use OpenapiFirst::Middlewares::RequestValidation, raise_error: true
|
|
88
|
+
```
|
|
87
89
|
|
|
88
|
-
|
|
90
|
+
#### Error responses
|
|
89
91
|
|
|
90
|
-
|
|
92
|
+
openapi_first produces a useful machine readable error response that can be customized.
|
|
93
|
+
The default response looks like this. See also [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457).
|
|
91
94
|
|
|
92
|
-
|
|
93
|
-
-
|
|
95
|
+
```json
|
|
96
|
+
http-status: 400
|
|
97
|
+
content-type: "application/problem+json"
|
|
94
98
|
|
|
95
|
-
|
|
99
|
+
{
|
|
100
|
+
"title": "Bad Request Body",
|
|
101
|
+
"status": 400,
|
|
102
|
+
"errors": [
|
|
103
|
+
{
|
|
104
|
+
"message": "value at `/data/name` is not a string",
|
|
105
|
+
"pointer": "/data/name",
|
|
106
|
+
"code": "string"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"message": "number at `/data/numberOfLegs` is less than: 2",
|
|
110
|
+
"pointer": "/data/numberOfLegs",
|
|
111
|
+
"code": "minimum"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"message": "object at `/data` is missing required properties: mandatory",
|
|
115
|
+
"pointer": "/data",
|
|
116
|
+
"code": "required"
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
```
|
|
96
121
|
|
|
97
|
-
|
|
122
|
+
openapi_first offers a [JSON:API](https://jsonapi.org/) error response by passing `error_response: :jsonapi`:
|
|
98
123
|
|
|
99
|
-
|
|
124
|
+
```ruby
|
|
125
|
+
use OpenapiFirst::Middlewares::RequestValidation, 'openapi.yaml', error_response: :jsonapi
|
|
126
|
+
```
|
|
100
127
|
|
|
101
|
-
|
|
128
|
+
<details>
|
|
129
|
+
<summary>See details of JSON:API error response</summary>
|
|
102
130
|
|
|
103
131
|
```json
|
|
104
|
-
http-status: 400
|
|
105
|
-
content-type: "application/vnd.api+json"
|
|
132
|
+
// http-status: 400
|
|
133
|
+
// content-type: "application/vnd.api+json"
|
|
106
134
|
|
|
107
135
|
{
|
|
108
136
|
"errors": [
|
|
109
137
|
{
|
|
110
|
-
"
|
|
138
|
+
"status": "400",
|
|
139
|
+
"source": {
|
|
140
|
+
"pointer": "/data/name"
|
|
141
|
+
},
|
|
142
|
+
"title": "value at `/data/name` is not a string",
|
|
143
|
+
"code": "string"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"status": "400",
|
|
147
|
+
"source": {
|
|
148
|
+
"pointer": "/data/numberOfLegs"
|
|
149
|
+
},
|
|
150
|
+
"title": "number at `/data/numberOfLegs` is less than: 2",
|
|
151
|
+
"code": "minimum"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"status": "400",
|
|
111
155
|
"source": {
|
|
112
|
-
"
|
|
113
|
-
}
|
|
156
|
+
"pointer": "/data"
|
|
157
|
+
},
|
|
158
|
+
"title": "object at `/data` is missing required properties: mandatory",
|
|
159
|
+
"code": "required"
|
|
114
160
|
}
|
|
115
161
|
]
|
|
116
162
|
}
|
|
117
163
|
```
|
|
118
164
|
|
|
119
|
-
|
|
165
|
+
</details>
|
|
166
|
+
|
|
167
|
+
#### Custom error responses
|
|
120
168
|
|
|
121
|
-
|
|
122
|
-
|
|
169
|
+
You can build your own custom error response with `error_response: MyCustomClass` that implements `OpenapiFirst::ErrorResponse`.
|
|
170
|
+
You can define custom error responses globally by including / implementing `OpenapiFirst::ErrorResponse` and register it via `OpenapiFirst.register_error_response(my_name, MyCustomErrorResponse)` and set `error_response: my_name`.
|
|
123
171
|
|
|
124
|
-
|
|
172
|
+
### Response validation
|
|
125
173
|
|
|
126
|
-
|
|
174
|
+
This middleware raises an error by default if the response is not valid.
|
|
175
|
+
This can be useful in a test or staging environment, especially if you are adopting OpenAPI for an existing implementation.
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
use OpenapiFirst::Middlewares::ResponseValidation if ENV['RACK_ENV'] == 'test'
|
|
179
|
+
|
|
180
|
+
# Pass `raise_error: false` to not raise an error:
|
|
181
|
+
use OpenapiFirst::Middlewares::ResponseValidation, raise_error: false
|
|
182
|
+
```
|
|
127
183
|
|
|
128
|
-
|
|
184
|
+
If you are adopting OpenAPI you can use these options together with [hooks](#hooks) to get notified about requests/responses that do match your API description.
|
|
129
185
|
|
|
130
|
-
|
|
131
|
-
This will add the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
|
|
186
|
+
## Contract Testing
|
|
132
187
|
|
|
133
|
-
|
|
188
|
+
You can see your OpenAPI API description as a contract that your clients can rely on as how your API behaves. There are two aspects of contract testing: Validation and Coverage. By validating requests and responses, you can avoid that your API implementation processes requests or returns responses that don't match your API description. To make sure your _whole_ API description is implemented, openapi_first can check that all of your API description is covered when you test your API with [rack-test](https://github.com/rack/rack-test).
|
|
134
189
|
|
|
135
|
-
|
|
190
|
+
Here is how to set it up:
|
|
136
191
|
|
|
137
|
-
|
|
192
|
+
1. Setup the test mode
|
|
193
|
+
```ruby
|
|
194
|
+
# spec_helper.rb
|
|
195
|
+
require 'openapi_first'
|
|
196
|
+
OpenapiFirst::Test.setup
|
|
197
|
+
```
|
|
198
|
+
2. Observe your application. You can do this in multiple ways:
|
|
199
|
+
- Add an `app` method to your tests (which is called by rack-test) that wraps your application with silent request / response validation.
|
|
200
|
+
```ruby
|
|
201
|
+
module RequestSpecHelpers
|
|
202
|
+
def app
|
|
203
|
+
OpenapiFirst::Test.app(MyApp)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
138
206
|
|
|
139
|
-
|
|
207
|
+
RSpec.configure do |config|
|
|
208
|
+
config.include RequestSpecHelpers, type: :request
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Or do this by creating a Module and including it to add an "app" method.
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
RSpec.configure do |config|
|
|
216
|
+
config.include OpenapiFirst::Test::Methods[MyApp], type: :request
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
3. Run your tests. The Coverage feature will tell you about missing or invalid requests/responses:
|
|
220
|
+
```
|
|
221
|
+
✓ GET /stations
|
|
222
|
+
✓ 200(application/json)
|
|
223
|
+
❌ 200(application/xml) – No responses tracked!
|
|
224
|
+
❌ 400(application/problem+json) – No responses tracked!
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Now add tests for all those "❌" to make them "✓" and you're green!
|
|
228
|
+
|
|
229
|
+
> [!NOTE]
|
|
230
|
+
> Check out [faraday-openapi](https://codeberg.org/ahx/faraday-openapi) to have your API _client_ validate request/responses against an OAD, which is useful to validate HTTP mocks during testing.
|
|
231
|
+
|
|
232
|
+
### Configure test coverage
|
|
233
|
+
|
|
234
|
+
OpenapiFirst::Test raises an error when a response status is not defined except for 404 and 500. You can change this:
|
|
140
235
|
|
|
141
236
|
```ruby
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
237
|
+
OpenapiFirst::Test.setup do |test|
|
|
238
|
+
test.ignored_unknown_status << 403
|
|
239
|
+
end
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Or you can ignore all unknown response status:
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
OpenapiFirst::Test.setup do |test|
|
|
246
|
+
test.ignore_unknown_response_status = true
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Exclude certain _responses_ from coverage with `skip_coverage`:
|
|
146
251
|
|
|
147
|
-
|
|
252
|
+
```ruby
|
|
253
|
+
OpenapiFirst::Test.setup do |test|
|
|
254
|
+
test.skip_response_coverage do |response_definition|
|
|
255
|
+
response_definition.status == '5XX'
|
|
256
|
+
end
|
|
257
|
+
end
|
|
148
258
|
```
|
|
149
259
|
|
|
150
|
-
|
|
260
|
+
Skip coverage for a request and all responses alltogether of a route with `skip_coverage`:
|
|
151
261
|
|
|
152
|
-
|
|
262
|
+
```ruby
|
|
263
|
+
OpenapiFirst::Test.setup do |test|
|
|
264
|
+
test.skip_coverage do |path, request_method|
|
|
265
|
+
path == '/bookings/{bookingId}' && requests_method == 'DELETE'
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
```
|
|
153
269
|
|
|
154
|
-
|
|
270
|
+
OpenapiFirst::Test raises an error when a request is not defined. You can deactivate this with:
|
|
155
271
|
|
|
156
272
|
```ruby
|
|
157
|
-
|
|
158
|
-
|
|
273
|
+
OpenapiFirst::Test.setup do |test|
|
|
274
|
+
test.ignore_unknown_requests = true
|
|
275
|
+
end
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Test assertions
|
|
279
|
+
|
|
280
|
+
> [!WARNING]
|
|
281
|
+
> You probably don't need this. Just setup [Contract testing and API coverage](#contract-testing) and use your normal assertions.
|
|
282
|
+
|
|
283
|
+
openapi_first ships with a simple but powerful Test method `assert_api_conform` to run request and response validation in your tests without using the middlewares. This is designed to be used with rack-test.
|
|
159
284
|
|
|
160
|
-
|
|
285
|
+
Here is how to use it with RSpec, but MiniTest works just as good:
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
# spec_helper.rb
|
|
289
|
+
OpenapiFirst::Test.setup do |test|
|
|
290
|
+
test.register(File.join(__dir__, '../examples/openapi.yaml'), as: :example_app)
|
|
291
|
+
end
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
Inside your test :
|
|
296
|
+
```ruby
|
|
297
|
+
RSpec.describe 'Example App' do
|
|
161
298
|
include Rack::Test::Methods
|
|
299
|
+
include OpenapiFirst::Test::Methods[App]
|
|
162
300
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
301
|
+
it 'is API conform' do
|
|
302
|
+
get '/'
|
|
303
|
+
assert_api_conform(status: 200)
|
|
166
304
|
end
|
|
305
|
+
end
|
|
306
|
+
```
|
|
167
307
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
308
|
+
## Manual use
|
|
309
|
+
|
|
310
|
+
Load the API description:
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
require 'openapi_first'
|
|
314
|
+
|
|
315
|
+
definition = OpenapiFirst.load('openapi.yaml')
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Validate request
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
validated_request = definition.validate_request(rack_request)
|
|
322
|
+
|
|
323
|
+
# Inspect the request and access parsed parameters
|
|
324
|
+
validated_request.valid?
|
|
325
|
+
validated_request.invalid?
|
|
326
|
+
validated_request.error # => Failure object or nil
|
|
327
|
+
validated_request.parsed_body # => The parsed request body (Hash)
|
|
328
|
+
validated_request.parsed_query # A Hash of query parameters that are defined in the API description, parsed exactly as described.
|
|
329
|
+
validated_request.parsed_path_parameters
|
|
330
|
+
validated_request.parsed_headers
|
|
331
|
+
validated_request.parsed_cookies
|
|
332
|
+
validated_request.parsed_params # Merged parsed path, query parameters and request body
|
|
333
|
+
# Access the Openapi 3 Operation Object Hash
|
|
334
|
+
validated_request.operation['x-foo']
|
|
335
|
+
validated_request.operation['operationId'] => "getStuff"
|
|
336
|
+
# or the whole request definition
|
|
337
|
+
validated_request.request_definition.path # => "/pets/{petId}"
|
|
338
|
+
validated_request.request_definition.operation_id # => "showPetById"
|
|
339
|
+
|
|
340
|
+
# Or you can raise an exception if validation fails:
|
|
341
|
+
definition.validate_request(rack_request, raise_error: true) # Raises OpenapiFirst::RequestInvalidError or OpenapiFirst::NotFoundError if request is invalid
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Validate response
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
validated_response = definition.validate_response(rack_request, rack_response)
|
|
348
|
+
|
|
349
|
+
# Inspect the response and access parsed parameters and
|
|
350
|
+
validated_response.valid?
|
|
351
|
+
validated_response.invalid?
|
|
352
|
+
validated_response.error # => Failure object or nil
|
|
353
|
+
validated_response.status # => 200
|
|
354
|
+
validated_response.parsed_body
|
|
355
|
+
validated_response.parsed_headers
|
|
356
|
+
|
|
357
|
+
# Or you can raise an exception if validation fails:
|
|
358
|
+
definition.validate_response(rack_request,rack_response, raise_error: true) # Raises OpenapiFirst::ResponseInvalidError or OpenapiFirst::ResponseNotFoundError
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Hooks
|
|
362
|
+
|
|
363
|
+
You can integrate your code at certain points during request/response validation via hooks.
|
|
364
|
+
|
|
365
|
+
Available hooks:
|
|
172
366
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
367
|
+
- `after_request_validation`
|
|
368
|
+
- `after_response_validation`
|
|
369
|
+
- `after_request_parameter_property_validation`
|
|
370
|
+
- `after_request_body_property_validation`
|
|
371
|
+
|
|
372
|
+
Setup per per instance:
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
OpenapiFirst.load('openapi.yaml') do |config|
|
|
376
|
+
config.after_request_validation do |validated_request|
|
|
377
|
+
validated_request.valid? # => true / false
|
|
378
|
+
end
|
|
379
|
+
config.after_response_validation do |validated_response, request|
|
|
380
|
+
if validated_response.invalid?
|
|
381
|
+
warn "#{request.request_method} #{request.path}: #{validated_response.error.message}"
|
|
382
|
+
end
|
|
176
383
|
end
|
|
384
|
+
end
|
|
385
|
+
```
|
|
177
386
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
387
|
+
Setup globally:
|
|
388
|
+
|
|
389
|
+
```ruby
|
|
390
|
+
OpenapiFirst.configure do |config|
|
|
391
|
+
config.after_request_parameter_property_validation do |data, property, property_schema|
|
|
392
|
+
data[property] = Date.iso8601(data[property]) if property_schema['format'] == 'date'
|
|
181
393
|
end
|
|
182
394
|
end
|
|
183
395
|
```
|
|
184
396
|
|
|
185
|
-
##
|
|
397
|
+
## Framework integration
|
|
186
398
|
|
|
187
|
-
|
|
399
|
+
Using rack middlewares is supported in probably all Ruby web frameworks.
|
|
400
|
+
|
|
401
|
+
The contract testing feature is designed to be used via rack-test, which should be compatible all Ruby web frameworks as well.
|
|
402
|
+
|
|
403
|
+
That aside, closer integration with specific frameworks like Sinatra, Hanami, Roda or others would be great. If you have ideas, pain points or PRs, please don't hesitate to [share](https://github.com/ahx/openapi_first/discussions).
|
|
188
404
|
|
|
189
405
|
## Alternatives
|
|
190
406
|
|
|
191
|
-
This gem
|
|
407
|
+
This gem was inspired by [committee](https://github.com/interagent/committee) (Ruby) and [Connexion](https://github.com/spec-first/connexion) (Python).
|
|
408
|
+
Here is a [feature comparison between openapi_first and committee](https://gist.github.com/ahx/1538c31f0652f459861713b5259e366a).
|
|
409
|
+
|
|
410
|
+
## Frequently Asked Questions
|
|
411
|
+
|
|
412
|
+
### How can I adapt request paths that don't match my schema?
|
|
413
|
+
|
|
414
|
+
Let's say you have `openapi.yaml` like this:
|
|
415
|
+
|
|
416
|
+
```yaml
|
|
417
|
+
servers:
|
|
418
|
+
- url: https://yourhost/api
|
|
419
|
+
paths:
|
|
420
|
+
# The actual endpoint URL is https://yourhost/api/resource
|
|
421
|
+
/resource:
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Here your OpenAPI schema defines endpoints starting with `/resource` but your actual application is mounted at `/api/resource`. You can bridge the gap by transforming the path via the `path:` configuration:
|
|
425
|
+
|
|
426
|
+
```ruby
|
|
427
|
+
oad = OpenapiFirst.load('openapi.yaml') do |config|
|
|
428
|
+
config.path = ->(req) { request.path.delete_prefix('/api') }
|
|
429
|
+
end
|
|
430
|
+
use OpenapiFirst::Middlewares::RequestValidation, oad
|
|
431
|
+
```
|
|
192
432
|
|
|
193
433
|
## Development
|
|
194
434
|
|
|
435
|
+
Run `git submodule update --init` to initialize the git submodules.
|
|
436
|
+
|
|
195
437
|
Run `bin/setup` to install dependencies.
|
|
196
438
|
|
|
197
|
-
|
|
439
|
+
See `bundle exec rake` to run the linter and the tests.
|
|
440
|
+
|
|
441
|
+
Run `bundle exec rspec` to run the tests only.
|
|
442
|
+
|
|
443
|
+
### Benchmarks
|
|
198
444
|
|
|
199
|
-
|
|
445
|
+
[Results](https://gist.github.com/ahx/e6ffced58bd2e8d5baffb2f4d2c1f823)
|
|
200
446
|
|
|
201
|
-
|
|
447
|
+
Run benchmarks:
|
|
202
448
|
|
|
203
449
|
```sh
|
|
204
450
|
cd benchmarks
|
|
@@ -206,8 +452,8 @@ bundle
|
|
|
206
452
|
bundle exec ruby benchmarks.rb
|
|
207
453
|
```
|
|
208
454
|
|
|
209
|
-
|
|
455
|
+
### Contributing
|
|
210
456
|
|
|
211
|
-
If you have a question or an idea or found a bug don't hesitate to
|
|
457
|
+
If you have a question or an idea or found a bug, don't hesitate to create an issue [on Github](https://github.com/ahx/openapi_first) or [Codeberg](https://codeberg.org/ahx/openapi_first) or say hi on [Mastodon (ruby.social)](https://ruby.social/@ahx).
|
|
212
458
|
|
|
213
459
|
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. 🤗
|