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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +585 -2
  3. data/LICENSE.txt +21 -0
  4. data/README.md +360 -114
  5. data/lib/openapi_first/builder.rb +214 -0
  6. data/lib/openapi_first/child_configuration.rb +26 -0
  7. data/lib/openapi_first/configuration.rb +76 -0
  8. data/lib/openapi_first/definition.rb +98 -13
  9. data/lib/openapi_first/error_response.rb +40 -0
  10. data/lib/openapi_first/error_responses/default.rb +74 -0
  11. data/lib/openapi_first/error_responses/jsonapi.rb +60 -0
  12. data/lib/openapi_first/errors.rb +40 -0
  13. data/lib/openapi_first/failure.rb +77 -0
  14. data/lib/openapi_first/file_loader.rb +22 -0
  15. data/lib/openapi_first/header.rb +9 -0
  16. data/lib/openapi_first/json.rb +25 -0
  17. data/lib/openapi_first/middlewares/request_validation.rb +58 -0
  18. data/lib/openapi_first/middlewares/response_validation.rb +41 -0
  19. data/lib/openapi_first/parsed_request.rb +5 -0
  20. data/lib/openapi_first/ref_resolver.rb +193 -0
  21. data/lib/openapi_first/registry.rb +44 -0
  22. data/lib/openapi_first/request.rb +79 -0
  23. data/lib/openapi_first/request_body_parsers.rb +64 -0
  24. data/lib/openapi_first/request_validator.rb +32 -0
  25. data/lib/openapi_first/response.rb +43 -0
  26. data/lib/openapi_first/response_body_parsers.rb +31 -0
  27. data/lib/openapi_first/response_parser.rb +50 -0
  28. data/lib/openapi_first/response_validator.rb +16 -48
  29. data/lib/openapi_first/router/find_content.rb +17 -0
  30. data/lib/openapi_first/router/find_response.rb +53 -0
  31. data/lib/openapi_first/router/path_template.rb +48 -0
  32. data/lib/openapi_first/router.rb +84 -57
  33. data/lib/openapi_first/schema/hash.rb +39 -0
  34. data/lib/openapi_first/schema/validation_error.rb +45 -0
  35. data/lib/openapi_first/schema/validation_result.rb +30 -0
  36. data/lib/openapi_first/test/app.rb +30 -0
  37. data/lib/openapi_first/test/callable.rb +25 -0
  38. data/lib/openapi_first/test/configuration.rb +76 -0
  39. data/lib/openapi_first/test/coverage/covered_request.rb +11 -0
  40. data/lib/openapi_first/test/coverage/covered_response.rb +11 -0
  41. data/lib/openapi_first/test/coverage/plan.rb +77 -0
  42. data/lib/openapi_first/test/coverage/request_task.rb +42 -0
  43. data/lib/openapi_first/test/coverage/response_task.rb +42 -0
  44. data/lib/openapi_first/test/coverage/route_task.rb +13 -0
  45. data/lib/openapi_first/test/coverage/terminal_formatter.rb +115 -0
  46. data/lib/openapi_first/test/coverage/tracker.rb +32 -0
  47. data/lib/openapi_first/test/coverage.rb +102 -0
  48. data/lib/openapi_first/test/methods.rb +62 -0
  49. data/lib/openapi_first/test/minitest_helpers.rb +28 -0
  50. data/lib/openapi_first/test/observe.rb +37 -0
  51. data/lib/openapi_first/test/observer_middleware.rb +23 -0
  52. data/lib/openapi_first/test/plain_helpers.rb +31 -0
  53. data/lib/openapi_first/test.rb +171 -0
  54. data/lib/openapi_first/validated_request.rb +79 -0
  55. data/lib/openapi_first/validated_response.rb +53 -0
  56. data/lib/openapi_first/validators/request_body.rb +25 -0
  57. data/lib/openapi_first/validators/request_parameters.rb +49 -0
  58. data/lib/openapi_first/validators/response_body.rb +27 -0
  59. data/lib/openapi_first/validators/response_headers.rb +40 -0
  60. data/lib/openapi_first/version.rb +1 -1
  61. data/lib/openapi_first.rb +67 -35
  62. metadata +99 -133
  63. data/.github/CODEOWNERS +0 -1
  64. data/.gitignore +0 -11
  65. data/.rspec +0 -3
  66. data/.rubocop.yml +0 -14
  67. data/.travis.yml +0 -8
  68. data/Gemfile +0 -11
  69. data/Gemfile.lock +0 -126
  70. data/Rakefile +0 -15
  71. data/benchmarks/Gemfile +0 -13
  72. data/benchmarks/Gemfile.lock +0 -144
  73. data/benchmarks/apps/committee.ru +0 -30
  74. data/benchmarks/apps/grape.ru +0 -21
  75. data/benchmarks/apps/hanami_router.ru +0 -14
  76. data/benchmarks/apps/openapi.yaml +0 -86
  77. data/benchmarks/apps/openapi_first.ru +0 -22
  78. data/benchmarks/apps/openapi_first_resolve_only.ru +0 -23
  79. data/benchmarks/apps/sinatra.ru +0 -26
  80. data/benchmarks/apps/syro.ru +0 -25
  81. data/benchmarks/benchmarks.rb +0 -46
  82. data/bin/console +0 -15
  83. data/bin/setup +0 -8
  84. data/examples/README.md +0 -13
  85. data/examples/app.rb +0 -12
  86. data/examples/config.ru +0 -7
  87. data/examples/openapi.yaml +0 -29
  88. data/lib/openapi_first/app.rb +0 -27
  89. data/lib/openapi_first/coverage.rb +0 -28
  90. data/lib/openapi_first/operation.rb +0 -48
  91. data/lib/openapi_first/operation_resolver.rb +0 -43
  92. data/lib/openapi_first/request_validation.rb +0 -144
  93. data/lib/openapi_first/utils.rb +0 -31
  94. data/lib/openapi_first/validation.rb +0 -15
  95. data/lib/openapi_first/validation_format.rb +0 -31
  96. data/openapi_first.gemspec +0 -46
data/README.md CHANGED
@@ -1,204 +1,450 @@
1
- # OpenapiFirst
1
+ # openapi_first
2
2
 
3
- OpenapiFirst helps to implement HTTP APIs based on an [OpenApi](https://www.openapis.org/) API description. The idea is that you create an API description first, then add a method that returns data and implements your business logic and be done.
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
- ## TL;DR
5
+ ## Usage
6
6
 
7
- Start with writing an OpenAPI file that describes the API, which you are about to write. Use a [validator](https://github.com/stoplightio/spectral/) to make sure the file is valid.
8
- In the following examples, the OpenAPI file is named `openapi/openapi.yaml`.
9
-
10
- Now implement your API:
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
- module Pets
14
- def self.find_pet(params, res)
15
- {
16
- id: params['id'],
17
- name: 'Oscar'
18
- }
19
- end
20
- end
14
+ use OpenapiFirst::Middlewares::RequestValidation
15
+ ```
21
16
 
22
- # In config.ru:
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
- run OpenapiFirst.app('./openapi/openapi.yaml', namespace: Pets)
25
- ```
21
+ OpenapiFirst::Test.setup
26
22
 
27
- The above will:
23
+ require 'my_app'
24
+ RSpec.configure do |config|
25
+ config.include OpenapiFirst::Test::Methods[MyApp], type: :request
26
+ end
27
+ ```
28
28
 
29
- - Validate the request and respond with 400 if the request does not match with your API description
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
- Handler functions (`find_pet`) are called with two arguments:
31
+ <!-- TOC -->
34
32
 
35
- - `params` - Holds the parsed request body, filtered query params and path parameters
36
- - `res` - Holds a Rack::Response that you can modify if needed
37
- If you want to access to plain Rack env you can call `params.env`.
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
- ### Handling only certain paths
48
+ <!-- /TOC -->
40
49
 
41
- You can filter the URIs that should be handled by pass ing `only` to `OpenapiFirst.load`:
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
- spec = OpenapiFirst.load './openapi/openapi.yaml', only: '/pets'.method(:==)
45
- run OpenapiFirst.app(spec, namespace: Pets)
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
- ### Usage as Rack middleware
60
+ You can configure default options globally:
49
61
 
50
62
  ```ruby
51
- # Just like the above, except the last line
52
- # ...
53
- run OpenapiFirst.middleware('./openapi/openapi.yaml', namespace: Pets)
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
- When using the middleware, all requests that are not part of the API description will be passed to the next app.
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
- gem 'openapi_first'
72
+ OpenapiFirst.load('openapi.yaml') do |config|
73
+ config.request_validation_error_response = :jsonapi
74
+ end
69
75
  ```
70
76
 
71
- OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
77
+ ## Rack Middlewares
72
78
 
73
- ## Handlers
79
+ ### Request validation
74
80
 
75
- OpenapiFirst maps the HTTP request to a method call based on the [operationId](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operation-object) in your API description.
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
- It works like this:
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
- - `params` - Holds the parsed request body, filtered query params and path parameters
86
- - `res` - Holds a Rack::Response that you can modify if needed
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
- You can call `params.env` to access the Rack env (just like in [Hanami actions](https://guides.hanamirb.org/actions/parameters/))
90
+ #### Error responses
89
91
 
90
- There are two ways to set the response body:
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
- - Calling `res.write "things"` (see [Rack::Response](https://www.rubydoc.info/github/rack/rack/Rack/Response))
93
- - Returning a value from the function (see example above) (this will always converted to JSON)
95
+ ```json
96
+ http-status: 400
97
+ content-type: "application/problem+json"
94
98
 
95
- ## Request validation
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
- If the request is not valid, these middlewares return a 400 status code with a body that describes the error.
122
+ openapi_first offers a [JSON:API](https://jsonapi.org/) error response by passing `error_response: :jsonapi`:
98
123
 
99
- The error responses conform with [JSON:API](https://jsonapi.org).
124
+ ```ruby
125
+ use OpenapiFirst::Middlewares::RequestValidation, 'openapi.yaml', error_response: :jsonapi
126
+ ```
100
127
 
101
- Here's an example response body for a missing query parameter "search":
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
- "title": "is missing",
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
- "parameter": "search"
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
- ### Parameter validation
165
+ </details>
166
+
167
+ #### Custom error responses
120
168
 
121
- The middleware filteres all top-level query parameters and paths parameters and tries to convert numeric values. Meaning, if you have an `:something_id` path with `type: integer`, it will try convert the value to an integer.
122
- Note that is currently does not convert date, date-time or time formats and that conversion is currently on done for path and query parameters, but not for request bodies.
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
- If you want to forbid _nested_ query parameters you will need to use [`additionalProperties: false`](https://json-schema.org/understanding-json-schema/reference/object.html#properties) in your query parameter JSON schema.
172
+ ### Response validation
125
173
 
126
- _OpenapiFirst always treats query parameters like [`style: deepObject`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#style-values), **but** it just works with nested objects (`filter[foo][bar]=baz`) (see [this discussion](https://github.com/OAI/OpenAPI-Specification/issues/1706))._
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
- ### Request body validation
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
- The middleware will return a `415` if the requests content type does not match or `400` if the request body is invalid.
131
- This will add the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
186
+ ## Contract Testing
132
187
 
133
- ### Header, Cookie, Path parameter validation
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
- tbd.
190
+ Here is how to set it up:
136
191
 
137
- ## Response validation
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
- Response validation is useful to make sure your app responds as described in your API description. You usually do this in your tests using [rack-test](https://github.com/rack-test/rack-test).
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
- # In your test (rspec example):
143
- require 'openapi_first'
144
- spec = OpenapiFirst.load('petstore.yaml')
145
- validator = OpenapiFirst::ResponseValidator.new(spec)
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
- expect(validator.validate(last_request, last_response).errors).to be_empty
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
- ## Coverage
260
+ Skip coverage for a request and all responses alltogether of a route with `skip_coverage`:
151
261
 
152
- (This is a bit experimental. Please try it out and give feedback.)
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
- `OpenapiFirst::Coverage` helps you make sure, that you have called all endpoints of your OAS file when running tests via `rack-test`.
270
+ OpenapiFirst::Test raises an error when a request is not defined. You can deactivate this with:
155
271
 
156
272
  ```ruby
157
- # In your test (rspec example):
158
- require 'openapi_first/coverage'
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
- describe MyApp do
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
- before(:all) do
164
- spec = OpenapiFirst.load('petstore.yaml')
165
- @app_wrapper = OpenapiFirst::Coverage.new(MyApp, spec)
301
+ it 'is API conform' do
302
+ get '/'
303
+ assert_api_conform(status: 200)
166
304
  end
305
+ end
306
+ ```
167
307
 
168
- after(:all) do
169
- message = "The following paths have not been called yet: #{@app_wrapper.to_be_called}"
170
- expect(@app_wrapper.to_be_called).to be_empty
171
- end
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
- # Overwrite `#app` to make rack-test call the wrapped app
174
- def app
175
- @app_wrapper
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
- it 'does things' do
179
- get '/i/my/stuff'
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
- ## Mocking
397
+ ## Framework integration
186
398
 
187
- Out of scope. Use [Prism](https://github.com/stoplightio/prism) or [fakeit](https://github.com/JustinFeng/fakeit).
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 is inspired by [committee](https://github.com/interagent/committee), which has much more features like response stubs or support for Hyper-Schema or OpenAPI 2.
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
- Run `bundle exec rspec` to run the tests.
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
- See `bundle exec rake -T` for rubygems related tasks.
445
+ [Results](https://gist.github.com/ahx/e6ffced58bd2e8d5baffb2f4d2c1f823)
200
446
 
201
- ### Run benchmarks
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
- ## Contributing
455
+ ### Contributing
210
456
 
211
- 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/issues).
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. 🤗