openapi_first 1.0.0.beta5 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +2 -1
- data/CHANGELOG.md +23 -2
- data/Gemfile +2 -0
- data/Gemfile.lock +16 -18
- data/Gemfile.rack2 +15 -0
- data/Gemfile.rack2.lock +99 -0
- data/README.md +99 -130
- data/lib/openapi_first/body_parser.rb +29 -0
- data/lib/openapi_first/configuration.rb +20 -0
- data/lib/openapi_first/definition/cookie_parameters.rb +12 -0
- data/lib/openapi_first/definition/header_parameters.rb +12 -0
- data/lib/openapi_first/definition/operation.rb +116 -0
- data/lib/openapi_first/definition/parameters.rb +47 -0
- data/lib/openapi_first/definition/path_item.rb +32 -0
- data/lib/openapi_first/definition/path_parameters.rb +12 -0
- data/lib/openapi_first/definition/query_parameters.rb +12 -0
- data/lib/openapi_first/definition/request_body.rb +43 -0
- data/lib/openapi_first/definition/response.rb +25 -0
- data/lib/openapi_first/definition/responses.rb +83 -0
- data/lib/openapi_first/definition.rb +61 -8
- data/lib/openapi_first/error_response.rb +22 -27
- data/lib/openapi_first/errors.rb +2 -14
- data/lib/openapi_first/failure.rb +55 -0
- data/lib/openapi_first/middlewares/request_validation.rb +52 -0
- data/lib/openapi_first/middlewares/response_validation.rb +35 -0
- data/lib/openapi_first/plugins/default/error_response.rb +74 -0
- data/lib/openapi_first/plugins/default.rb +11 -0
- data/lib/openapi_first/plugins/jsonapi/error_response.rb +58 -0
- data/lib/openapi_first/plugins/jsonapi.rb +11 -0
- data/lib/openapi_first/plugins.rb +9 -7
- data/lib/openapi_first/request_validation/request_body_validator.rb +41 -0
- data/lib/openapi_first/request_validation/validator.rb +81 -0
- data/lib/openapi_first/response_validation/validator.rb +101 -0
- data/lib/openapi_first/runtime_request.rb +84 -0
- data/lib/openapi_first/runtime_response.rb +31 -0
- data/lib/openapi_first/schema/validation_error.rb +18 -0
- data/lib/openapi_first/schema/validation_result.rb +32 -0
- data/lib/openapi_first/{json_schema.rb → schema.rb} +9 -5
- data/lib/openapi_first/version.rb +1 -1
- data/lib/openapi_first.rb +32 -28
- data/openapi_first.gemspec +10 -9
- metadata +55 -67
- data/.rspec +0 -3
- data/.rubocop.yml +0 -14
- data/Rakefile +0 -15
- data/benchmarks/Gemfile +0 -16
- data/benchmarks/Gemfile.lock +0 -142
- data/benchmarks/README.md +0 -29
- data/benchmarks/apps/committee_with_hanami_api.ru +0 -26
- data/benchmarks/apps/committee_with_response_validation.ru +0 -29
- data/benchmarks/apps/committee_with_sinatra.ru +0 -31
- data/benchmarks/apps/grape.ru +0 -21
- data/benchmarks/apps/hanami_api.ru +0 -21
- data/benchmarks/apps/hanami_router.ru +0 -14
- data/benchmarks/apps/openapi.yaml +0 -268
- data/benchmarks/apps/openapi_first_with_hanami_api.ru +0 -24
- data/benchmarks/apps/openapi_first_with_plain_rack.ru +0 -32
- data/benchmarks/apps/openapi_first_with_response_validation.ru +0 -25
- data/benchmarks/apps/openapi_first_with_sinatra.ru +0 -29
- data/benchmarks/apps/roda.ru +0 -27
- data/benchmarks/apps/sinatra.ru +0 -26
- data/benchmarks/apps/syro.ru +0 -25
- data/benchmarks/benchmark-wrk.sh +0 -3
- data/benchmarks/benchmarks.rb +0 -48
- data/benchmarks/post.lua +0 -3
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/examples/README.md +0 -13
- data/examples/app.rb +0 -18
- data/examples/config.ru +0 -7
- data/examples/openapi.yaml +0 -29
- data/lib/openapi_first/body_parser_middleware.rb +0 -40
- data/lib/openapi_first/config.rb +0 -20
- data/lib/openapi_first/error_responses/default.rb +0 -58
- data/lib/openapi_first/error_responses/json_api.rb +0 -58
- data/lib/openapi_first/json_schema/result.rb +0 -17
- data/lib/openapi_first/operation.rb +0 -170
- data/lib/openapi_first/request_body_validator.rb +0 -41
- data/lib/openapi_first/request_validation.rb +0 -118
- data/lib/openapi_first/request_validation_error.rb +0 -31
- data/lib/openapi_first/response_validation.rb +0 -93
- data/lib/openapi_first/response_validator.rb +0 -21
- data/lib/openapi_first/router.rb +0 -102
- data/lib/openapi_first/string_keyed_hash.rb +0 -20
- data/lib/openapi_first/use_router.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 259995bd946bd53afc1b6ab3cc2d556811a8d775ba2714d1e13f0058f3049048
|
4
|
+
data.tar.gz: fe150a639f4ddbbf6190296c0fb70ee06c5116711a2716826934fc12b48b1622
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed09d09b5ad4c131134843ed0c7772eb6742b9bd98e6bb410d2c7fca3efe9a49875993b095a9590ee1a5ec9484301d5a58ed67b445fdd46aacbabedaf7e8160e
|
7
|
+
data.tar.gz: 9fe5c08c823aff23d979b9cd0652e4a63a8df424f98c037cb77e45c28c7fd232f24305df8024495b7bd50f4b99fbb8669d0f8a962b20ef2652907298ac7dd6cf
|
data/.github/workflows/ruby.yml
CHANGED
@@ -9,4 +9,5 @@ jobs:
|
|
9
9
|
with:
|
10
10
|
ruby-version: '3.1'
|
11
11
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
12
|
-
- run: bundle exec rake
|
12
|
+
- run: BUNDLE_GEMFILE=Gemfile bundle exec rake
|
13
|
+
- run: BUNDLE_GEMFILE=Gemfile.rack2 bundle lock --add-platform x86_64-linux && bundle exec rake
|
data/CHANGELOG.md
CHANGED
@@ -1,11 +1,32 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
##
|
3
|
+
## 1.0.0
|
4
|
+
|
5
|
+
- Breaking: The default error uses application/problem+json content-type
|
6
|
+
- Breaking: Moved rack middlewares to OpenapiFirst::Middlewares
|
7
|
+
- Breaking: Rename OpenapiFirst::ResponseInvalid to OpenapiFirst::ResponseInvalidError
|
8
|
+
- Breaking: Remove OpenapiFirst::Router
|
9
|
+
- Breaking: Remove `env[OpenapiFirst::OPERATION]`. Use `env[OpenapiFirst::REQUEST]` instead.
|
10
|
+
- Breaking: Remove `env[OpenapiFirst::REQUEST_BODY]`, `env[OpenapiFirst::PARAMS]`. Use `env[OpenapiFirst::REQUEST].body env[OpenapiFirst::REQUEST].params` instead.
|
11
|
+
- Add interface to validate requests / responses without middlewares (see "Manual validation" in README)
|
12
|
+
- Add OpenapiFirst.configure
|
13
|
+
- Add OpenapiFirst.register, OpenapiFirst.plugin
|
14
|
+
- Fix response header validation with Rack 3
|
15
|
+
- Fixed: Add support for paths like `/{a}..{b}`
|
16
|
+
|
17
|
+
## 1.0.0.beta6
|
18
|
+
|
19
|
+
- Fix: Make response header validation work with rack 3
|
20
|
+
- Refactor router
|
21
|
+
- Remove dependency hanami-router
|
22
|
+
- PathItem and Operation for a request can be found by calling methods on the Definitnion
|
23
|
+
- Fixed https://github.com/ahx/openapi_first/issues/155
|
24
|
+
- Breaking / Regression: A paths like /pets/{from}-{to} if there is a path "/pets/{id}"
|
4
25
|
|
5
26
|
## 1.0.0.beta5
|
6
27
|
|
7
28
|
- Added: `OpenapiFirst::Config.default_options=` to set default options globally
|
8
|
-
- Added: You can define custom error responses by subclassing `OpenapiFirst::ErrorResponse` and register it via `OpenapiFirst
|
29
|
+
- Added: You can define custom error responses by subclassing `OpenapiFirst::ErrorResponse` and register it via `OpenapiFirst.register_error_response(name, MyCustomErrorResponse)`
|
9
30
|
|
10
31
|
## 1.0.0.beta4
|
11
32
|
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
openapi_first (1.0.0
|
5
|
-
hanami-router (~> 2.0.0)
|
4
|
+
openapi_first (1.0.0)
|
6
5
|
json_refs (~> 0.1, >= 0.1.7)
|
7
|
-
json_schemer (~> 2.
|
6
|
+
json_schemer (~> 2.1.0)
|
8
7
|
multi_json (~> 1.15)
|
9
|
-
|
8
|
+
mustermann-contrib (~> 3.0.0)
|
9
|
+
openapi_parameters (>= 0.3.2, < 2.0)
|
10
10
|
rack (>= 2.2, < 4.0)
|
11
11
|
|
12
12
|
GEM
|
@@ -15,15 +15,11 @@ GEM
|
|
15
15
|
ast (2.4.2)
|
16
16
|
diff-lcs (1.5.0)
|
17
17
|
hana (1.3.7)
|
18
|
-
hanami-router (2.0.2)
|
19
|
-
mustermann (~> 3.0)
|
20
|
-
mustermann-contrib (~> 3.0)
|
21
|
-
rack (~> 2.0)
|
22
18
|
hansi (0.2.1)
|
23
|
-
json (2.
|
19
|
+
json (2.7.1)
|
24
20
|
json_refs (0.1.8)
|
25
21
|
hana
|
26
|
-
json_schemer (2.
|
22
|
+
json_schemer (2.1.1)
|
27
23
|
hana (~> 1.3)
|
28
24
|
regexp_parser (~> 2.0)
|
29
25
|
simpleidn (~> 0.2)
|
@@ -34,20 +30,20 @@ GEM
|
|
34
30
|
mustermann-contrib (3.0.0)
|
35
31
|
hansi (~> 0.2.0)
|
36
32
|
mustermann (= 3.0.0)
|
37
|
-
openapi_parameters (0.3.
|
33
|
+
openapi_parameters (0.3.2)
|
38
34
|
rack (>= 2.2)
|
39
35
|
zeitwerk (~> 2.6)
|
40
|
-
parallel (1.
|
41
|
-
parser (3.
|
36
|
+
parallel (1.24.0)
|
37
|
+
parser (3.3.0.0)
|
42
38
|
ast (~> 2.4.1)
|
43
39
|
racc
|
44
40
|
racc (1.7.3)
|
45
|
-
rack (
|
41
|
+
rack (3.0.8)
|
46
42
|
rack-test (2.1.0)
|
47
43
|
rack (>= 1.3)
|
48
44
|
rainbow (3.1.1)
|
49
45
|
rake (13.1.0)
|
50
|
-
regexp_parser (2.8.
|
46
|
+
regexp_parser (2.8.3)
|
51
47
|
rexml (3.2.6)
|
52
48
|
rspec (3.12.0)
|
53
49
|
rspec-core (~> 3.12.0)
|
@@ -62,7 +58,7 @@ GEM
|
|
62
58
|
diff-lcs (>= 1.2.0, < 2.0)
|
63
59
|
rspec-support (~> 3.12.0)
|
64
60
|
rspec-support (3.12.1)
|
65
|
-
rubocop (1.
|
61
|
+
rubocop (1.59.0)
|
66
62
|
json (~> 2.3)
|
67
63
|
language_server-protocol (>= 3.17.0)
|
68
64
|
parallel (~> 1.10)
|
@@ -70,7 +66,7 @@ GEM
|
|
70
66
|
rainbow (>= 2.2.2, < 4.0)
|
71
67
|
regexp_parser (>= 1.8, < 3.0)
|
72
68
|
rexml (>= 3.2.5, < 4.0)
|
73
|
-
rubocop-ast (>= 1.
|
69
|
+
rubocop-ast (>= 1.30.0, < 2.0)
|
74
70
|
ruby-progressbar (~> 1.7)
|
75
71
|
unicode-display_width (>= 2.4.0, < 3.0)
|
76
72
|
rubocop-ast (1.30.0)
|
@@ -81,17 +77,19 @@ GEM
|
|
81
77
|
unf (~> 0.1.4)
|
82
78
|
unf (0.1.4)
|
83
79
|
unf_ext
|
84
|
-
unf_ext (0.0.9)
|
80
|
+
unf_ext (0.0.9.1)
|
85
81
|
unicode-display_width (2.5.0)
|
86
82
|
zeitwerk (2.6.12)
|
87
83
|
|
88
84
|
PLATFORMS
|
89
85
|
arm64-darwin-21
|
86
|
+
arm64-darwin-22
|
90
87
|
x86_64-linux
|
91
88
|
|
92
89
|
DEPENDENCIES
|
93
90
|
bundler
|
94
91
|
openapi_first!
|
92
|
+
rack (>= 3.0.0)
|
95
93
|
rack-test
|
96
94
|
rake
|
97
95
|
rspec
|
data/Gemfile.rack2
ADDED
data/Gemfile.rack2.lock
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
openapi_first (1.0.0.beta6)
|
5
|
+
json_refs (~> 0.1, >= 0.1.7)
|
6
|
+
json_schemer (~> 2.1.0)
|
7
|
+
multi_json (~> 1.15)
|
8
|
+
mustermann-contrib (~> 3.0.0)
|
9
|
+
openapi_parameters (>= 0.3.2, < 2.0)
|
10
|
+
rack (>= 2.2, < 4.0)
|
11
|
+
|
12
|
+
GEM
|
13
|
+
remote: https://rubygems.org/
|
14
|
+
specs:
|
15
|
+
ast (2.4.2)
|
16
|
+
diff-lcs (1.5.0)
|
17
|
+
hana (1.3.7)
|
18
|
+
hansi (0.2.1)
|
19
|
+
json (2.7.1)
|
20
|
+
json_refs (0.1.8)
|
21
|
+
hana
|
22
|
+
json_schemer (2.1.1)
|
23
|
+
hana (~> 1.3)
|
24
|
+
regexp_parser (~> 2.0)
|
25
|
+
simpleidn (~> 0.2)
|
26
|
+
language_server-protocol (3.17.0.3)
|
27
|
+
multi_json (1.15.0)
|
28
|
+
mustermann (3.0.0)
|
29
|
+
ruby2_keywords (~> 0.0.1)
|
30
|
+
mustermann-contrib (3.0.0)
|
31
|
+
hansi (~> 0.2.0)
|
32
|
+
mustermann (= 3.0.0)
|
33
|
+
openapi_parameters (0.3.2)
|
34
|
+
rack (>= 2.2)
|
35
|
+
zeitwerk (~> 2.6)
|
36
|
+
parallel (1.24.0)
|
37
|
+
parser (3.2.2.4)
|
38
|
+
ast (~> 2.4.1)
|
39
|
+
racc
|
40
|
+
racc (1.7.3)
|
41
|
+
rack (2.2.8)
|
42
|
+
rack-test (2.1.0)
|
43
|
+
rack (>= 1.3)
|
44
|
+
rainbow (3.1.1)
|
45
|
+
rake (13.1.0)
|
46
|
+
regexp_parser (2.8.3)
|
47
|
+
rexml (3.2.6)
|
48
|
+
rspec (3.12.0)
|
49
|
+
rspec-core (~> 3.12.0)
|
50
|
+
rspec-expectations (~> 3.12.0)
|
51
|
+
rspec-mocks (~> 3.12.0)
|
52
|
+
rspec-core (3.12.2)
|
53
|
+
rspec-support (~> 3.12.0)
|
54
|
+
rspec-expectations (3.12.3)
|
55
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
56
|
+
rspec-support (~> 3.12.0)
|
57
|
+
rspec-mocks (3.12.6)
|
58
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
59
|
+
rspec-support (~> 3.12.0)
|
60
|
+
rspec-support (3.12.1)
|
61
|
+
rubocop (1.59.0)
|
62
|
+
json (~> 2.3)
|
63
|
+
language_server-protocol (>= 3.17.0)
|
64
|
+
parallel (~> 1.10)
|
65
|
+
parser (>= 3.2.2.4)
|
66
|
+
rainbow (>= 2.2.2, < 4.0)
|
67
|
+
regexp_parser (>= 1.8, < 3.0)
|
68
|
+
rexml (>= 3.2.5, < 4.0)
|
69
|
+
rubocop-ast (>= 1.30.0, < 2.0)
|
70
|
+
ruby-progressbar (~> 1.7)
|
71
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
72
|
+
rubocop-ast (1.30.0)
|
73
|
+
parser (>= 3.2.1.0)
|
74
|
+
ruby-progressbar (1.13.0)
|
75
|
+
ruby2_keywords (0.0.5)
|
76
|
+
simpleidn (0.2.1)
|
77
|
+
unf (~> 0.1.4)
|
78
|
+
unf (0.1.4)
|
79
|
+
unf_ext
|
80
|
+
unf_ext (0.0.9.1)
|
81
|
+
unicode-display_width (2.5.0)
|
82
|
+
zeitwerk (2.6.12)
|
83
|
+
|
84
|
+
PLATFORMS
|
85
|
+
arm64-darwin-22
|
86
|
+
ruby
|
87
|
+
x86_64-linux
|
88
|
+
|
89
|
+
DEPENDENCIES
|
90
|
+
bundler
|
91
|
+
openapi_first!
|
92
|
+
rack (< 3.0.0)
|
93
|
+
rack-test
|
94
|
+
rake
|
95
|
+
rspec
|
96
|
+
rubocop
|
97
|
+
|
98
|
+
BUNDLED WITH
|
99
|
+
2.5.3
|
data/README.md
CHANGED
@@ -1,175 +1,144 @@
|
|
1
|
-
#
|
1
|
+
# openapi_first
|
2
2
|
|
3
|
-
|
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 to ensure that your implementation follows exactly the API description.
|
4
4
|
|
5
|
-
|
5
|
+
This makes your API description reliable, reason about API design and use various tooling on top of OpenAPI.
|
6
6
|
|
7
|
-
|
7
|
+
## Contents
|
8
8
|
|
9
|
-
|
10
|
-
- [`OpenapiFirst::ResponseValidation`](#response-validation) Validates the response and raises an exception if the response body is invalid.
|
11
|
-
- [`OpenapiFirst::Router`](#openapifirstrouter) – This internal middleware is added automatically when using request/response validation. It adds the OpenAPI operation for the current request to the Rack env.
|
9
|
+
<!-- TOC -->
|
12
10
|
|
13
|
-
|
11
|
+
- [Manual use](#manual-use)
|
12
|
+
- [Rack Middlewares](#rack-middlewares)
|
13
|
+
- [Configuration](#configuration)
|
14
|
+
- [Development](#development)
|
14
15
|
|
15
|
-
|
16
|
+
<!-- /TOC -->
|
16
17
|
|
17
|
-
|
18
|
+
## Manual use
|
18
19
|
|
19
|
-
|
20
|
-
use OpenapiFirst::RequestValidation, spec: 'openapi.yaml'
|
21
|
-
```
|
22
|
-
|
23
|
-
It adds these fields to the Rack env:
|
24
|
-
|
25
|
-
- `env[OpenapiFirst::PARAMS]` – The parsed parameters (query, path) for the current request (string keyed)
|
26
|
-
- `env[OpenapiFirst::REQUEST_BODY]` – The parsed request body (string keyed)
|
27
|
-
- `env[OpenapiFirst::OPERATION]` (Added via Router) – The Operation object for the current request. This is an instance of `OpenapiFirst::Operation`.
|
28
|
-
|
29
|
-
### Options and defaults
|
30
|
-
|
31
|
-
| Name | Possible values | Description | Default |
|
32
|
-
| :---------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------- |
|
33
|
-
| `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` |
|
34
|
-
| `raise_error:` | `false`, `true` | If set to true the middleware raises `OpenapiFirst::RequestInvalidError` instead of returning 4xx. | `false` (don't raise an exception) |
|
35
|
-
| `error_response:` | `:default`, `:json_api`, Your implementation of `ErrorResponse` | :default |
|
36
|
-
|
37
|
-
The error responses conform with [JSON:API](https://jsonapi.org).
|
38
|
-
|
39
|
-
Here's an example response body for a missing query parameter "search":
|
20
|
+
Load the API description:
|
40
21
|
|
41
|
-
```
|
42
|
-
|
43
|
-
content-type: "application/json"
|
22
|
+
```ruby
|
23
|
+
require 'openapi_first'
|
44
24
|
|
45
|
-
|
46
|
-
"errors": [
|
47
|
-
{
|
48
|
-
"title": "is missing",
|
49
|
-
"source": {
|
50
|
-
"parameter": "search"
|
51
|
-
}
|
52
|
-
}
|
53
|
-
]
|
54
|
-
}
|
25
|
+
definition = OpenapiFirst.load('petstore.yaml')
|
55
26
|
```
|
56
27
|
|
57
|
-
|
58
|
-
|
59
|
-
The `RequestValidation` middleware adds `env[OpenapiFirst::PARAMS]` (or `env['openapi.params']` ) with the converted query and path parameters. This only includes the 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. So you can do things these:
|
28
|
+
Validate request / response:
|
60
29
|
|
61
30
|
```ruby
|
62
|
-
# GET /pets/filter[id]=1,2,3
|
63
|
-
env[OpenapiFirst::PARAMS] # => { 'filter[id]' => [1,2,3] }
|
64
31
|
|
65
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
|
32
|
+
# Find the request
|
33
|
+
rack_request = Rack::Request.new(env)
|
34
|
+
request = definition.request(rack_request)
|
35
|
+
|
36
|
+
# Inspect the request and access parsed parameters
|
37
|
+
request.known? # Is the request defined in the API description?
|
38
|
+
request.parsed_body # alias: body
|
39
|
+
request.path_parameters
|
40
|
+
request.query # alias: query_parameters
|
41
|
+
request.params # Merged path and query parameters
|
42
|
+
request.headers
|
43
|
+
request.cookies
|
44
|
+
|
45
|
+
# Validate the request
|
46
|
+
request.validate # Returns OpenapiFirst:::Failure if validation fails
|
47
|
+
request.validate! # Raises OpenapiFirst::RequestInvalidError or OpenapiFirst::NotFoundError if validation fails
|
48
|
+
|
49
|
+
# Find the response
|
50
|
+
rack_response = Rack::Response[*app.call(env)]
|
51
|
+
response = request.response(rack_response) # or definition.response(rack_request, rack_response)
|
52
|
+
|
53
|
+
# Validate response
|
54
|
+
response.validate # Returns OpenapiFirst::Failure
|
55
|
+
response.validate! # Raises OpenapiFirst::ResponseInvalidError or OpenapiFirst::ResponseNotFoundError if validation fails
|
69
56
|
```
|
70
57
|
|
71
|
-
|
72
|
-
|
73
|
-
### Request body validation
|
74
|
-
|
75
|
-
This middleware adds the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
|
76
|
-
|
77
|
-
The middleware will return a status `415` if the requests content type does not match or `400` if the request body is invalid.
|
78
|
-
|
79
|
-
### Header, Cookie, Query and Path parameter validation
|
80
|
-
|
81
|
-
The `RequestValidation` middleware validates the request headers, cookies and path parameters as defined in you API description. It returns a `400` status code if the request is invalid. It adds the parsed merged _path_ and _query_ parameters to `env['openapi.params']`.
|
82
|
-
Separate parsed parameters are made available by location at `env['openapi.path_params']`, `env['openapi.query']`, `env['openapi.headers']`, `env['openapi.cookies']` as well if you need to access them separately.
|
83
|
-
|
84
|
-
### readOnly / writeOnly properties
|
85
|
-
|
86
|
-
Request validation fails if request includes a property with `readOnly: true`.
|
87
|
-
|
88
|
-
Response validation fails if response body includes a property with `writeOnly: true`.
|
89
|
-
|
90
|
-
## Response validation
|
91
|
-
|
92
|
-
The `OpenapiFirst::ResponseValidation` middleware is especially useful when testing. It _always_ raises an error if the response is not valid.
|
58
|
+
OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
|
93
59
|
|
94
|
-
|
95
|
-
use OpenapiFirst::ResponseValidation, spec: 'openapi.yaml' if ENV['RACK_ENV'] == 'test'
|
96
|
-
```
|
60
|
+
## Rack Middlewares
|
97
61
|
|
98
|
-
|
62
|
+
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.
|
99
63
|
|
100
|
-
|
101
|
-
| :------ | --------------- | ---------------------------------------------------------------- | ------- |
|
102
|
-
| `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` |
|
64
|
+
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.
|
103
65
|
|
104
|
-
|
66
|
+
### Request validation
|
105
67
|
|
106
|
-
|
68
|
+
The request validation middleware returns a 4xx if the request is invalid or not defined in the API description.
|
107
69
|
|
108
70
|
```ruby
|
109
|
-
use OpenapiFirst::
|
71
|
+
use OpenapiFirst::Middlewares::RequestValidation, spec: 'openapi.yaml'
|
110
72
|
```
|
111
73
|
|
112
|
-
|
74
|
+
#### Options
|
113
75
|
|
114
|
-
|
76
|
+
| Name | Possible values | Description |
|
77
|
+
| :---------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
78
|
+
| `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` |
|
79
|
+
| `raise_error:` | `false` (default), `true` | If set to true the middleware raises `OpenapiFirst::RequestInvalidError` or `OpenapiFirst::NotFoundError` instead of returning 4xx. |
|
80
|
+
| `error_response:` | `:default` (default), `:json_api`, Your implementation of `ErrorResponse` | :default |
|
115
81
|
|
116
|
-
|
117
|
-
| :------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
|
118
|
-
| `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` | |
|
119
|
-
| `raise_error:` | `false`, `true` | If set to true the middleware raises `OpenapiFirst::NotFoundError` when a path or method was not found in the API description. This is useful during testing to spot an incomplete API description. | `false` (don't raise an exception) |
|
120
|
-
| `not_found:` | `:continue`, `:halt` | If set to `:continue` the middleware will not return 404 (405, 415), but just pass handling the request to the next middleware or application in the Rack stack. If combined with `raise_error: true` `raise_error` gets preference and an exception is raised. | `:halt` (return 4xx response) |
|
82
|
+
Here's an example response body about an invalid request body. See also [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457).
|
121
83
|
|
122
|
-
|
123
|
-
|
124
|
-
|
84
|
+
```json
|
85
|
+
http-status: 400
|
86
|
+
content-type: "application/problem+json"
|
125
87
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
88
|
+
{
|
89
|
+
"title": "Bad Request Body",
|
90
|
+
"status": 400,
|
91
|
+
"errors": [
|
92
|
+
{
|
93
|
+
"message": "value at `/data/name` is not a string",
|
94
|
+
"pointer": "/data/name",
|
95
|
+
"code": "string"
|
96
|
+
},
|
97
|
+
{
|
98
|
+
"message": "number at `/data/numberOfLegs` is less than: 2",
|
99
|
+
"pointer": "/data/numberOfLegs",
|
100
|
+
"code": "minimum"
|
101
|
+
},
|
102
|
+
{
|
103
|
+
"message": "object at `/data` is missing required properties: mandatory",
|
104
|
+
"pointer": "/data",
|
105
|
+
"code": "required"
|
106
|
+
}
|
107
|
+
]
|
130
108
|
}
|
131
109
|
```
|
132
110
|
|
133
|
-
|
134
|
-
|
135
|
-
This gem is inspired by [committee](https://github.com/interagent/committee) (Ruby) and [connexion](https://github.com/zalando/connexion) (Python).
|
136
|
-
|
137
|
-
Here's a [comparison between committee and openapi_first](https://gist.github.com/ahx/1538c31f0652f459861713b5259e366a).
|
111
|
+
#### readOnly / writeOnly properties
|
138
112
|
|
139
|
-
|
113
|
+
Request validation fails if request includes a property with `readOnly: true`.
|
140
114
|
|
141
|
-
|
115
|
+
Response validation fails if response body includes a property with `writeOnly: true`.
|
142
116
|
|
143
|
-
|
117
|
+
### Response validation
|
144
118
|
|
145
|
-
|
119
|
+
This middleware is especially useful when testing. It _always_ raises an error if the response is not valid.
|
146
120
|
|
147
121
|
```ruby
|
148
|
-
|
122
|
+
use OpenapiFirst::Middlewares::ResponseValidation, spec: 'openapi.yaml' if ENV['RACK_ENV'] == 'test'
|
149
123
|
```
|
150
124
|
|
151
|
-
|
152
|
-
|
153
|
-
## Manual response validation
|
154
|
-
|
155
|
-
Instead of using the ResponseValidation middleware you can validate the response in your test manually via [rack-test](https://github.com/rack-test/rack-test) and ResponseValidator.
|
125
|
+
#### Options
|
156
126
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
validator = OpenapiFirst::ResponseValidator.new('petstore.yaml')
|
161
|
-
|
162
|
-
# This will raise an exception if it found an error
|
163
|
-
validator.validate(last_request, last_response)
|
164
|
-
```
|
127
|
+
| Name | Possible values | Description |
|
128
|
+
| :------ | --------------- | ---------------------------------------------------------------- |
|
129
|
+
| `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` |
|
165
130
|
|
166
|
-
##
|
131
|
+
## Configuration
|
167
132
|
|
168
|
-
You can
|
133
|
+
You can configure default options globally:
|
169
134
|
|
170
135
|
```ruby
|
171
|
-
|
172
|
-
|
136
|
+
OpenapiFirst.configure do |config|
|
137
|
+
# Specify which plugin is used to render error responses returned by the request validation middleware (defaults to :default)
|
138
|
+
config.request_validation_error_response = :json_api
|
139
|
+
# Configure if the response validation middleware should raise an exception (defaults to false)
|
140
|
+
config.request_validation_raise_error = true
|
141
|
+
end
|
173
142
|
```
|
174
143
|
|
175
144
|
## Development
|
@@ -180,11 +149,11 @@ See `bundle exec rake` to run the linter and the tests.
|
|
180
149
|
|
181
150
|
Run `bundle exec rspec` to run the tests only.
|
182
151
|
|
183
|
-
|
152
|
+
### Benchmarks
|
184
153
|
|
185
154
|
[Results](https://gist.github.com/ahx/e6ffced58bd2e8d5baffb2f4d2c1f823)
|
186
155
|
|
187
|
-
|
156
|
+
Run benchmarks:
|
188
157
|
|
189
158
|
```sh
|
190
159
|
cd benchmarks
|
@@ -192,8 +161,8 @@ bundle
|
|
192
161
|
bundle exec ruby benchmarks.rb
|
193
162
|
```
|
194
163
|
|
195
|
-
|
164
|
+
### Contributing
|
196
165
|
|
197
|
-
If you have a question or an idea or found a bug don't hesitate to [create an issue
|
166
|
+
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).
|
198
167
|
|
199
168
|
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. 🤗
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'multi_json'
|
4
|
+
|
5
|
+
module OpenapiFirst
|
6
|
+
class BodyParser
|
7
|
+
class ParsingError < StandardError; end
|
8
|
+
|
9
|
+
def parse(request, content_type)
|
10
|
+
body = read_body(request)
|
11
|
+
return if body.empty?
|
12
|
+
|
13
|
+
return MultiJson.load(body) if content_type =~ (/json/i) && (content_type =~ /json/i)
|
14
|
+
return request.POST if request.form_data?
|
15
|
+
|
16
|
+
body
|
17
|
+
rescue MultiJson::ParseError
|
18
|
+
raise ParsingError, 'Failed to parse body as JSON'
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def read_body(request)
|
24
|
+
body = request.body.read
|
25
|
+
request.body.rewind
|
26
|
+
body
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
class Configuration
|
5
|
+
def initialize
|
6
|
+
@request_validation_error_response = OpenapiFirst.plugin(:default)::ErrorResponse
|
7
|
+
@request_validation_raise_error = false
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :request_validation_error_response, :request_validation_raise_error
|
11
|
+
|
12
|
+
def request_validation_error_response=(mod)
|
13
|
+
@request_validation_error_response = if mod.is_a?(Symbol)
|
14
|
+
OpenapiFirst.plugin(:default)::ErrorResponse
|
15
|
+
else
|
16
|
+
mod
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openapi_parameters'
|
4
|
+
require_relative 'parameters'
|
5
|
+
|
6
|
+
module OpenapiFirst
|
7
|
+
class CookieParameters < Parameters
|
8
|
+
def unpack(env)
|
9
|
+
OpenapiParameters::Cookie.new(@parameter_definitions).unpack(env['HTTP_COOKIE'])
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openapi_parameters'
|
4
|
+
require_relative 'parameters'
|
5
|
+
|
6
|
+
module OpenapiFirst
|
7
|
+
class HeaderParameters < Parameters
|
8
|
+
def unpack(env)
|
9
|
+
OpenapiParameters::Header.new(@parameter_definitions).unpack_env(env)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|