openapi_first 0.20.0 → 0.21.0
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/.rubocop.yml +1 -1
- data/CHANGELOG.md +9 -1
- data/Gemfile.lock +35 -29
- data/README.md +4 -5
- data/benchmarks/Gemfile.lock +52 -53
- data/benchmarks/benchmarks.rb +2 -1
- data/lib/openapi_first/body_parser_middleware.rb +53 -0
- data/lib/openapi_first/errors.rb +2 -0
- data/lib/openapi_first/operation.rb +24 -0
- data/lib/openapi_first/request_validation.rb +42 -40
- data/lib/openapi_first/router.rb +40 -14
- data/lib/openapi_first/schema_validation.rb +9 -0
- data/lib/openapi_first/use_router.rb +1 -3
- data/lib/openapi_first/version.rb +1 -1
- data/openapi_first.gemspec +3 -3
- metadata +12 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b8b03aaa251de1bdb5cbba71de089bf1ffc6b9dbb96512008f88e783c3cea27
|
|
4
|
+
data.tar.gz: 02eb6cec864e9b5ed272d4392b31fd9e006713d3a8b00df9b26e9a52fa68e555
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e37e99e982f0ead9d54587683fa74491e69998f520b1cfb4993c9e1ee81273537dfdf1751c77daef856ab3c3051660589d2f8ac6e58508fd11d3ea130734d2a5
|
|
7
|
+
data.tar.gz: 6944ae2444da29928eeb12e70326505aa154f1c36a09f033083098b1e34be766b075257fd2459b94574370fa8cc417ef488f74dafcec6026eaab07a47f471445
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.21.0
|
|
6
|
+
|
|
7
|
+
- Fix: Query parameter validation does not fail if header parameters are defined (Thanks to [JF Lalonde](https://github.com/JF-Lalonde))
|
|
8
|
+
- Update Ruby dependency to >= 3.0.5
|
|
9
|
+
- Handle simple form-data in request bodies (see https://github.com/ahx/openapi_first/issues/149)
|
|
10
|
+
- Update to hanami-router 2.0.0 stable
|
|
11
|
+
|
|
5
12
|
## 0.20.0
|
|
13
|
+
|
|
6
14
|
- You can pass a filepath to `spec:` now so you no longer have to call `OpenapiFirst.load` anymore.
|
|
7
15
|
- Router is optional now.
|
|
8
16
|
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.
|
|
@@ -49,7 +57,7 @@ Yanked. No useful changes.
|
|
|
49
57
|
|
|
50
58
|
## 0.14.1
|
|
51
59
|
|
|
52
|
-
-
|
|
60
|
+
- Fix: Don't mix path- and operation-level parameters for request validation
|
|
53
61
|
|
|
54
62
|
## 0.14.0
|
|
55
63
|
|
data/Gemfile.lock
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
openapi_first (0.
|
|
4
|
+
openapi_first (0.21.0)
|
|
5
5
|
deep_merge (>= 1.2.1)
|
|
6
|
-
hanami-router (
|
|
7
|
-
hanami-utils (
|
|
6
|
+
hanami-router (~> 2.0.0)
|
|
7
|
+
hanami-utils (~> 2.0.0)
|
|
8
8
|
json_refs (~> 0.1, >= 0.1.7)
|
|
9
9
|
json_schemer (~> 0.2.16)
|
|
10
10
|
multi_json (~> 1.14)
|
|
@@ -15,78 +15,84 @@ GEM
|
|
|
15
15
|
specs:
|
|
16
16
|
ast (2.4.2)
|
|
17
17
|
coderay (1.1.3)
|
|
18
|
-
concurrent-ruby (1.
|
|
18
|
+
concurrent-ruby (1.2.2)
|
|
19
19
|
deep_merge (1.2.2)
|
|
20
20
|
diff-lcs (1.5.0)
|
|
21
|
-
dry-
|
|
21
|
+
dry-core (1.0.0)
|
|
22
|
+
concurrent-ruby (~> 1.0)
|
|
23
|
+
zeitwerk (~> 2.6)
|
|
24
|
+
dry-transformer (1.0.1)
|
|
25
|
+
zeitwerk (~> 2.6)
|
|
22
26
|
ecma-re-validator (0.4.0)
|
|
23
27
|
regexp_parser (~> 2.2)
|
|
24
28
|
hana (1.3.7)
|
|
25
|
-
hanami-router (2.0.
|
|
26
|
-
mustermann (~>
|
|
27
|
-
mustermann-contrib (~>
|
|
29
|
+
hanami-router (2.0.2)
|
|
30
|
+
mustermann (~> 3.0)
|
|
31
|
+
mustermann-contrib (~> 3.0)
|
|
28
32
|
rack (~> 2.0)
|
|
29
|
-
hanami-utils (2.0.
|
|
33
|
+
hanami-utils (2.0.3)
|
|
30
34
|
concurrent-ruby (~> 1.0)
|
|
31
|
-
dry-
|
|
35
|
+
dry-core (~> 1.0, < 2)
|
|
36
|
+
dry-transformer (~> 1.0, < 2)
|
|
32
37
|
hansi (0.2.1)
|
|
33
|
-
json (2.6.
|
|
38
|
+
json (2.6.3)
|
|
34
39
|
json_refs (0.1.7)
|
|
35
40
|
hana
|
|
36
|
-
json_schemer (0.2.
|
|
41
|
+
json_schemer (0.2.24)
|
|
37
42
|
ecma-re-validator (~> 0.3)
|
|
38
43
|
hana (~> 1.3)
|
|
39
44
|
regexp_parser (~> 2.0)
|
|
40
45
|
uri_template (~> 0.7)
|
|
41
46
|
method_source (1.0.0)
|
|
42
47
|
multi_json (1.15.0)
|
|
43
|
-
mustermann (
|
|
48
|
+
mustermann (3.0.0)
|
|
44
49
|
ruby2_keywords (~> 0.0.1)
|
|
45
|
-
mustermann-contrib (
|
|
50
|
+
mustermann-contrib (3.0.0)
|
|
46
51
|
hansi (~> 0.2.0)
|
|
47
|
-
mustermann (=
|
|
52
|
+
mustermann (= 3.0.0)
|
|
48
53
|
parallel (1.22.1)
|
|
49
|
-
parser (3.
|
|
54
|
+
parser (3.2.1.0)
|
|
50
55
|
ast (~> 2.4.1)
|
|
51
|
-
pry (0.14.
|
|
56
|
+
pry (0.14.2)
|
|
52
57
|
coderay (~> 1.1)
|
|
53
58
|
method_source (~> 1.0)
|
|
54
|
-
rack (2.2.
|
|
59
|
+
rack (2.2.6.2)
|
|
55
60
|
rack-test (1.1.0)
|
|
56
61
|
rack (>= 1.0, < 3)
|
|
57
62
|
rainbow (3.1.1)
|
|
58
63
|
rake (13.0.6)
|
|
59
|
-
regexp_parser (2.
|
|
64
|
+
regexp_parser (2.7.0)
|
|
60
65
|
rexml (3.2.5)
|
|
61
66
|
rspec (3.12.0)
|
|
62
67
|
rspec-core (~> 3.12.0)
|
|
63
68
|
rspec-expectations (~> 3.12.0)
|
|
64
69
|
rspec-mocks (~> 3.12.0)
|
|
65
|
-
rspec-core (3.12.
|
|
70
|
+
rspec-core (3.12.1)
|
|
66
71
|
rspec-support (~> 3.12.0)
|
|
67
|
-
rspec-expectations (3.12.
|
|
72
|
+
rspec-expectations (3.12.2)
|
|
68
73
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
69
74
|
rspec-support (~> 3.12.0)
|
|
70
|
-
rspec-mocks (3.12.
|
|
75
|
+
rspec-mocks (3.12.3)
|
|
71
76
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
72
77
|
rspec-support (~> 3.12.0)
|
|
73
78
|
rspec-support (3.12.0)
|
|
74
|
-
rubocop (1.
|
|
79
|
+
rubocop (1.45.1)
|
|
75
80
|
json (~> 2.3)
|
|
76
81
|
parallel (~> 1.10)
|
|
77
|
-
parser (>= 3.
|
|
82
|
+
parser (>= 3.2.0.0)
|
|
78
83
|
rainbow (>= 2.2.2, < 4.0)
|
|
79
84
|
regexp_parser (>= 1.8, < 3.0)
|
|
80
85
|
rexml (>= 3.2.5, < 4.0)
|
|
81
|
-
rubocop-ast (>= 1.
|
|
86
|
+
rubocop-ast (>= 1.24.1, < 2.0)
|
|
82
87
|
ruby-progressbar (~> 1.7)
|
|
83
|
-
unicode-display_width (>=
|
|
84
|
-
rubocop-ast (1.
|
|
85
|
-
parser (>= 3.
|
|
88
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
|
89
|
+
rubocop-ast (1.26.0)
|
|
90
|
+
parser (>= 3.2.1.0)
|
|
86
91
|
ruby-progressbar (1.11.0)
|
|
87
92
|
ruby2_keywords (0.0.5)
|
|
88
|
-
unicode-display_width (2.
|
|
93
|
+
unicode-display_width (2.4.2)
|
|
89
94
|
uri_template (0.7.0)
|
|
95
|
+
zeitwerk (2.6.7)
|
|
90
96
|
|
|
91
97
|
PLATFORMS
|
|
92
98
|
arm64-darwin-21
|
data/README.md
CHANGED
|
@@ -32,7 +32,7 @@ And these Rack apps:
|
|
|
32
32
|
This middleware returns a 400 status code with a body that describes the error if the request is not valid.
|
|
33
33
|
|
|
34
34
|
```ruby
|
|
35
|
-
use OpenapiFirst::
|
|
35
|
+
use OpenapiFirst::RequestValidation, spec: 'openapi.yaml'
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
### Options and defaults
|
|
@@ -110,7 +110,7 @@ use OpenapiFirst::ResponseValidation, spec: 'openapi.yaml' if ENV['RACK_ENV'] ==
|
|
|
110
110
|
|
|
111
111
|
## OpenapiFirst::Router
|
|
112
112
|
|
|
113
|
-
This middleware
|
|
113
|
+
This middleware is used automatically, but you can add it to the top of your middleware stack if you want to change configuration.
|
|
114
114
|
|
|
115
115
|
```ruby
|
|
116
116
|
use OpenapiFirst::Router, spec: './openapi/openapi.yaml'
|
|
@@ -122,8 +122,7 @@ This middleware adds `env[OpenapiFirst::OPERATION]` which holds an Operation obj
|
|
|
122
122
|
|
|
123
123
|
| Name | Possible values | Description | Default |
|
|
124
124
|
| :------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
|
|
125
|
-
| `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load`
|
|
126
|
-
| |
|
|
125
|
+
| `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` | |
|
|
127
126
|
| `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) |
|
|
128
127
|
| `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) |
|
|
129
128
|
|
|
@@ -269,7 +268,7 @@ validator.validate(last_request, last_response)
|
|
|
269
268
|
You can filter the URIs that should be handled by passing `only` to `OpenapiFirst.load`:
|
|
270
269
|
|
|
271
270
|
```ruby
|
|
272
|
-
spec = OpenapiFirst.load('./openapi/openapi.yaml', only: '/pets'
|
|
271
|
+
spec = OpenapiFirst.load('./openapi/openapi.yaml', only: { |path| path.starts_with? '/pets' })
|
|
273
272
|
run OpenapiFirst.app(spec, namespace: Pets)
|
|
274
273
|
```
|
|
275
274
|
|
data/benchmarks/Gemfile.lock
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
openapi_first (0.
|
|
4
|
+
openapi_first (0.21.0)
|
|
5
5
|
deep_merge (>= 1.2.1)
|
|
6
|
-
hanami-router (
|
|
7
|
-
hanami-utils (
|
|
6
|
+
hanami-router (~> 2.0.0)
|
|
7
|
+
hanami-utils (~> 2.0.0)
|
|
8
8
|
json_refs (~> 0.1, >= 0.1.7)
|
|
9
9
|
json_schemer (~> 0.2.16)
|
|
10
10
|
multi_json (~> 1.14)
|
|
@@ -13,43 +13,40 @@ PATH
|
|
|
13
13
|
GEM
|
|
14
14
|
remote: https://rubygems.org/
|
|
15
15
|
specs:
|
|
16
|
-
activesupport (7.0.2
|
|
16
|
+
activesupport (7.0.4.2)
|
|
17
17
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
18
18
|
i18n (>= 1.6, < 2)
|
|
19
19
|
minitest (>= 5.1)
|
|
20
20
|
tzinfo (~> 2.0)
|
|
21
|
-
benchmark-ips (2.
|
|
21
|
+
benchmark-ips (2.11.0)
|
|
22
22
|
benchmark-memory (0.2.0)
|
|
23
23
|
memory_profiler (~> 1)
|
|
24
24
|
builder (3.2.4)
|
|
25
|
-
committee (
|
|
25
|
+
committee (5.0.0)
|
|
26
26
|
json_schema (~> 0.14, >= 0.14.3)
|
|
27
|
-
openapi_parser (
|
|
27
|
+
openapi_parser (~> 1.0)
|
|
28
28
|
rack (>= 1.5)
|
|
29
|
-
concurrent-ruby (1.
|
|
29
|
+
concurrent-ruby (1.2.0)
|
|
30
30
|
deep_merge (1.2.2)
|
|
31
|
-
dry-
|
|
31
|
+
dry-core (1.0.0)
|
|
32
32
|
concurrent-ruby (~> 1.0)
|
|
33
|
-
|
|
34
|
-
dry-
|
|
33
|
+
zeitwerk (~> 2.6)
|
|
34
|
+
dry-inflector (1.0.0)
|
|
35
|
+
dry-logic (1.5.0)
|
|
35
36
|
concurrent-ruby (~> 1.0)
|
|
36
|
-
dry-
|
|
37
|
-
|
|
37
|
+
dry-core (~> 1.0, < 2)
|
|
38
|
+
zeitwerk (~> 2.6)
|
|
39
|
+
dry-transformer (1.0.1)
|
|
40
|
+
zeitwerk (~> 2.6)
|
|
41
|
+
dry-types (1.7.0)
|
|
38
42
|
concurrent-ruby (~> 1.0)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
dry-transformer (0.1.1)
|
|
44
|
-
dry-types (1.5.1)
|
|
45
|
-
concurrent-ruby (~> 1.0)
|
|
46
|
-
dry-container (~> 0.3)
|
|
47
|
-
dry-core (~> 0.5, >= 0.5)
|
|
48
|
-
dry-inflector (~> 0.1, >= 0.1.2)
|
|
49
|
-
dry-logic (~> 1.0, >= 1.0.2)
|
|
43
|
+
dry-core (~> 1.0, < 2)
|
|
44
|
+
dry-inflector (~> 1.0, < 2)
|
|
45
|
+
dry-logic (>= 1.4, < 2)
|
|
46
|
+
zeitwerk (~> 2.6)
|
|
50
47
|
ecma-re-validator (0.4.0)
|
|
51
48
|
regexp_parser (~> 2.2)
|
|
52
|
-
grape (1.
|
|
49
|
+
grape (1.7.0)
|
|
53
50
|
activesupport
|
|
54
51
|
builder
|
|
55
52
|
dry-types (>= 1.1)
|
|
@@ -57,62 +54,64 @@ GEM
|
|
|
57
54
|
rack (>= 1.3.0)
|
|
58
55
|
rack-accept
|
|
59
56
|
hana (1.3.7)
|
|
60
|
-
hanami-api (0.
|
|
61
|
-
hanami-router (~> 2.0
|
|
62
|
-
hanami-router (2.0.
|
|
63
|
-
mustermann (~>
|
|
64
|
-
mustermann-contrib (~>
|
|
57
|
+
hanami-api (0.3.0)
|
|
58
|
+
hanami-router (~> 2.0)
|
|
59
|
+
hanami-router (2.0.2)
|
|
60
|
+
mustermann (~> 3.0)
|
|
61
|
+
mustermann-contrib (~> 3.0)
|
|
65
62
|
rack (~> 2.0)
|
|
66
|
-
hanami-utils (2.0.
|
|
63
|
+
hanami-utils (2.0.3)
|
|
67
64
|
concurrent-ruby (~> 1.0)
|
|
68
|
-
dry-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
dry-core (~> 1.0, < 2)
|
|
66
|
+
dry-transformer (~> 1.0, < 2)
|
|
67
|
+
hansi (0.2.1)
|
|
68
|
+
i18n (1.12.0)
|
|
71
69
|
concurrent-ruby (~> 1.0)
|
|
72
70
|
json_refs (0.1.7)
|
|
73
71
|
hana
|
|
74
72
|
json_schema (0.21.0)
|
|
75
|
-
json_schemer (0.2.
|
|
73
|
+
json_schemer (0.2.24)
|
|
76
74
|
ecma-re-validator (~> 0.3)
|
|
77
75
|
hana (~> 1.3)
|
|
78
76
|
regexp_parser (~> 2.0)
|
|
79
77
|
uri_template (~> 0.7)
|
|
80
|
-
memory_profiler (1.0.
|
|
81
|
-
minitest (5.
|
|
78
|
+
memory_profiler (1.0.1)
|
|
79
|
+
minitest (5.17.0)
|
|
82
80
|
multi_json (1.15.0)
|
|
83
|
-
mustermann (
|
|
81
|
+
mustermann (3.0.0)
|
|
84
82
|
ruby2_keywords (~> 0.0.1)
|
|
85
|
-
mustermann-contrib (
|
|
83
|
+
mustermann-contrib (3.0.0)
|
|
86
84
|
hansi (~> 0.2.0)
|
|
87
|
-
mustermann (=
|
|
88
|
-
mustermann-grape (1.0.
|
|
85
|
+
mustermann (= 3.0.0)
|
|
86
|
+
mustermann-grape (1.0.2)
|
|
89
87
|
mustermann (>= 1.0.0)
|
|
90
88
|
nio4r (2.5.8)
|
|
91
|
-
openapi_parser (0.
|
|
92
|
-
puma (
|
|
89
|
+
openapi_parser (1.0.0)
|
|
90
|
+
puma (6.1.0)
|
|
93
91
|
nio4r (~> 2.0)
|
|
94
|
-
rack (2.2.
|
|
92
|
+
rack (2.2.6.2)
|
|
95
93
|
rack-accept (0.4.5)
|
|
96
94
|
rack (>= 0.4)
|
|
97
|
-
rack-protection (
|
|
95
|
+
rack-protection (3.0.5)
|
|
98
96
|
rack
|
|
99
|
-
regexp_parser (2.
|
|
100
|
-
roda (3.
|
|
97
|
+
regexp_parser (2.7.0)
|
|
98
|
+
roda (3.65.0)
|
|
101
99
|
rack
|
|
102
100
|
ruby2_keywords (0.0.5)
|
|
103
101
|
seg (1.2.0)
|
|
104
|
-
sinatra (
|
|
105
|
-
mustermann (~>
|
|
106
|
-
rack (~> 2.2)
|
|
107
|
-
rack-protection (=
|
|
102
|
+
sinatra (3.0.5)
|
|
103
|
+
mustermann (~> 3.0)
|
|
104
|
+
rack (~> 2.2, >= 2.2.4)
|
|
105
|
+
rack-protection (= 3.0.5)
|
|
108
106
|
tilt (~> 2.0)
|
|
109
107
|
syro (3.2.1)
|
|
110
108
|
rack (>= 1.6.0)
|
|
111
109
|
seg
|
|
112
|
-
tilt (2.0.
|
|
113
|
-
tzinfo (2.0.
|
|
110
|
+
tilt (2.0.11)
|
|
111
|
+
tzinfo (2.0.6)
|
|
114
112
|
concurrent-ruby (~> 1.0)
|
|
115
113
|
uri_template (0.7.0)
|
|
114
|
+
zeitwerk (2.6.7)
|
|
116
115
|
|
|
117
116
|
PLATFORMS
|
|
118
117
|
arm64-darwin-21
|
data/benchmarks/benchmarks.rb
CHANGED
|
@@ -18,7 +18,8 @@ examples = [
|
|
|
18
18
|
[Rack::MockRequest.env_for('/hello?filter[id]=1,2'), 200]
|
|
19
19
|
]
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
glob = ARGV[0] || './apps/*.ru'
|
|
22
|
+
apps = Dir[glob].each_with_object({}) do |config, hash|
|
|
22
23
|
hash[config] = Rack::Builder.parse_file(config).first
|
|
23
24
|
end
|
|
24
25
|
apps.freeze
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'multi_json'
|
|
4
|
+
|
|
5
|
+
module OpenapiFirst
|
|
6
|
+
class BodyParserMiddleware
|
|
7
|
+
def initialize(app, options = {})
|
|
8
|
+
@app = app
|
|
9
|
+
@raise = options.fetch(:raise_error, false)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
RACK_INPUT = 'rack.input'
|
|
13
|
+
ROUTER_PARSED_BODY = 'router.parsed_body'
|
|
14
|
+
|
|
15
|
+
def call(env)
|
|
16
|
+
env[ROUTER_PARSED_BODY] = parse_body(env)
|
|
17
|
+
@app.call(env)
|
|
18
|
+
rescue BodyParsingError => e
|
|
19
|
+
raise if @raise
|
|
20
|
+
|
|
21
|
+
err = { title: "Failed to parse body as #{env['CONTENT_TYPE']}", status: '400' }
|
|
22
|
+
err[:detail] = e.cause unless ENV['RACK_ENV'] == 'production'
|
|
23
|
+
errors = [err]
|
|
24
|
+
|
|
25
|
+
Rack::Response.new(
|
|
26
|
+
MultiJson.dump(errors: errors),
|
|
27
|
+
400,
|
|
28
|
+
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
|
29
|
+
).finish
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def parse_body(env)
|
|
35
|
+
request = Rack::Request.new(env)
|
|
36
|
+
body = read_body(request)
|
|
37
|
+
return if body.empty?
|
|
38
|
+
|
|
39
|
+
return MultiJson.load(body) if request.media_type =~ (/json/i) && (request.media_type =~ /json/i)
|
|
40
|
+
return request.POST if request.form_data?
|
|
41
|
+
|
|
42
|
+
body
|
|
43
|
+
rescue MultiJson::ParseError => e
|
|
44
|
+
raise BodyParsingError, e
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def read_body(request)
|
|
48
|
+
body = request.body.read
|
|
49
|
+
request.body.rewind
|
|
50
|
+
body
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/openapi_first/errors.rb
CHANGED
|
@@ -47,6 +47,13 @@ module OpenapiFirst
|
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
def query_parameters_schema
|
|
51
|
+
@query_parameters_schema ||= begin
|
|
52
|
+
query_parameters_json_schema = build_query_parameters_json_schema
|
|
53
|
+
query_parameters_json_schema && SchemaValidation.new(query_parameters_json_schema)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
50
57
|
def content_types_for(status)
|
|
51
58
|
response_for(status)['content']&.keys
|
|
52
59
|
end
|
|
@@ -88,6 +95,13 @@ module OpenapiFirst
|
|
|
88
95
|
"#{method.upcase} #{path} (#{operation_id})"
|
|
89
96
|
end
|
|
90
97
|
|
|
98
|
+
def valid_request_content_type?(request_content_type)
|
|
99
|
+
content = operation_object.dig('requestBody', 'content')
|
|
100
|
+
return unless content
|
|
101
|
+
|
|
102
|
+
!!find_content_for_content_type(content, request_content_type)
|
|
103
|
+
end
|
|
104
|
+
|
|
91
105
|
private
|
|
92
106
|
|
|
93
107
|
def response_by_code(status)
|
|
@@ -118,6 +132,16 @@ module OpenapiFirst
|
|
|
118
132
|
end
|
|
119
133
|
end
|
|
120
134
|
|
|
135
|
+
def build_query_parameters_json_schema
|
|
136
|
+
query_parameters = all_parameters.reject { |field, _value| field['in'] == 'header' }
|
|
137
|
+
return unless query_parameters&.any?
|
|
138
|
+
|
|
139
|
+
query_parameters.each_with_object(new_node) do |parameter, schema|
|
|
140
|
+
params = Rack::Utils.parse_nested_query(parameter['name'])
|
|
141
|
+
generate_schema(schema, params, parameter)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
121
145
|
def all_parameters
|
|
122
146
|
parameters = @path_item_object['parameters']&.dup || []
|
|
123
147
|
parameters_on_operation = operation_object['parameters']
|
|
@@ -20,57 +20,56 @@ module OpenapiFirst
|
|
|
20
20
|
return @app.call(env) unless operation
|
|
21
21
|
|
|
22
22
|
env[INBOX] = {}
|
|
23
|
-
catch(:
|
|
24
|
-
validate_query_parameters!(
|
|
23
|
+
error = catch(:error) do
|
|
24
|
+
params = validate_query_parameters!(operation, env[PARAMETERS])
|
|
25
|
+
env[INBOX].merge! env[PARAMETERS] = params if params
|
|
25
26
|
req = Rack::Request.new(env)
|
|
26
|
-
content_type = req.content_type
|
|
27
27
|
return @app.call(env) unless operation.request_body
|
|
28
28
|
|
|
29
|
-
validate_request_content_type!(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
validate_request_content_type!(operation, req.content_type)
|
|
30
|
+
parsed_request_body = parse_and_validate_request_body!(operation, req)
|
|
31
|
+
env[REQUEST_BODY] = parsed_request_body
|
|
32
|
+
env[INBOX].merge! parsed_request_body if parsed_request_body.is_a?(Hash)
|
|
33
|
+
nil
|
|
34
34
|
end
|
|
35
|
+
if error
|
|
36
|
+
raise RequestInvalidError, error[:errors] if @raise
|
|
37
|
+
|
|
38
|
+
return validation_error_response(error[:status], error[:errors])
|
|
39
|
+
end
|
|
40
|
+
@app.call(env)
|
|
35
41
|
end
|
|
36
42
|
|
|
37
43
|
private
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
ROUTER_PARSED_BODY = 'router.parsed_body'
|
|
46
|
+
|
|
47
|
+
def parse_and_validate_request_body!(operation, request)
|
|
48
|
+
env = request.env
|
|
49
|
+
|
|
50
|
+
body = env.delete(ROUTER_PARSED_BODY) if env.key?(ROUTER_PARSED_BODY)
|
|
42
51
|
|
|
43
|
-
def parse_and_validate_request_body!(env, content_type, body, operation)
|
|
44
52
|
validate_request_body_presence!(body, operation)
|
|
45
|
-
return if body.
|
|
53
|
+
return if body.nil?
|
|
46
54
|
|
|
47
|
-
schema = operation&.request_body_schema(content_type)
|
|
55
|
+
schema = operation&.request_body_schema(request.content_type)
|
|
48
56
|
return unless schema
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
errors
|
|
52
|
-
|
|
53
|
-
env[INBOX].merge! env[REQUEST_BODY] = Utils.deep_symbolize(parsed_request_body)
|
|
54
|
-
end
|
|
58
|
+
errors = schema.validate(body)
|
|
59
|
+
throw_error(400, serialize_request_body_errors(errors)) if errors.any?
|
|
60
|
+
return Utils.deep_symbolize(body) if body.is_a?(Hash)
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
MultiJson.load(body)
|
|
58
|
-
rescue MultiJson::ParseError => e
|
|
59
|
-
err = { title: 'Failed to parse body as JSON' }
|
|
60
|
-
err[:detail] = e.cause unless ENV['RACK_ENV'] == 'production'
|
|
61
|
-
halt_with_error(400, [err])
|
|
62
|
+
body
|
|
62
63
|
end
|
|
63
64
|
|
|
64
|
-
def validate_request_content_type!(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
halt_with_error(415)
|
|
65
|
+
def validate_request_content_type!(operation, content_type)
|
|
66
|
+
operation.valid_request_content_type?(content_type) || throw_error(415)
|
|
68
67
|
end
|
|
69
68
|
|
|
70
69
|
def validate_request_body_presence!(body, operation)
|
|
71
|
-
return unless operation.request_body['required'] && body.
|
|
70
|
+
return unless operation.request_body['required'] && body.nil?
|
|
72
71
|
|
|
73
|
-
|
|
72
|
+
throw_error(415, 'Request body is required')
|
|
74
73
|
end
|
|
75
74
|
|
|
76
75
|
def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
|
|
@@ -80,10 +79,15 @@ module OpenapiFirst
|
|
|
80
79
|
}
|
|
81
80
|
end
|
|
82
81
|
|
|
83
|
-
def
|
|
84
|
-
|
|
82
|
+
def throw_error(status, errors = [default_error(status)])
|
|
83
|
+
throw :error, {
|
|
84
|
+
status: status,
|
|
85
|
+
errors: errors
|
|
86
|
+
}
|
|
87
|
+
end
|
|
85
88
|
|
|
86
|
-
|
|
89
|
+
def validation_error_response(status, errors)
|
|
90
|
+
Rack::Response.new(
|
|
87
91
|
MultiJson.dump(errors: errors),
|
|
88
92
|
status,
|
|
89
93
|
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
|
@@ -100,17 +104,15 @@ module OpenapiFirst
|
|
|
100
104
|
end
|
|
101
105
|
end
|
|
102
106
|
|
|
103
|
-
def validate_query_parameters!(
|
|
104
|
-
schema = operation.
|
|
107
|
+
def validate_query_parameters!(operation, params)
|
|
108
|
+
schema = operation.query_parameters_schema
|
|
105
109
|
return unless schema
|
|
106
110
|
|
|
107
111
|
params = filtered_params(schema.raw_schema, params)
|
|
108
112
|
params = Utils.deep_stringify(params)
|
|
109
113
|
errors = schema.validate(params)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
env[PARAMETERS] = params
|
|
113
|
-
env[INBOX].merge! params
|
|
114
|
+
throw_error(400, serialize_query_parameter_errors(errors)) if errors.any?
|
|
115
|
+
Utils.deep_symbolize(params)
|
|
114
116
|
end
|
|
115
117
|
|
|
116
118
|
def filtered_params(json_schema, params)
|
data/lib/openapi_first/router.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'rack'
|
|
4
|
+
require 'multi_json'
|
|
4
5
|
require 'hanami/router'
|
|
6
|
+
require_relative 'body_parser_middleware'
|
|
5
7
|
|
|
6
8
|
module OpenapiFirst
|
|
7
9
|
class Router
|
|
@@ -54,26 +56,50 @@ module OpenapiFirst
|
|
|
54
56
|
env[ORIGINAL_PATH] = env[Rack::PATH_INFO]
|
|
55
57
|
env[Rack::PATH_INFO] = Rack::Request.new(env).path
|
|
56
58
|
@router.call(env)
|
|
59
|
+
rescue BodyParsingError => e
|
|
60
|
+
handle_body_parsing_error(e)
|
|
57
61
|
ensure
|
|
58
62
|
env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
|
|
59
63
|
end
|
|
60
64
|
|
|
65
|
+
def handle_body_parsing_error(exception)
|
|
66
|
+
err = { title: 'Failed to parse body as application/json', status: '400' }
|
|
67
|
+
err[:detail] = exception.cause unless ENV['RACK_ENV'] == 'production'
|
|
68
|
+
errors = [err]
|
|
69
|
+
raise RequestInvalidError, errors if @raise
|
|
70
|
+
|
|
71
|
+
Rack::Response.new(
|
|
72
|
+
MultiJson.dump(errors: errors),
|
|
73
|
+
400,
|
|
74
|
+
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
|
75
|
+
).finish
|
|
76
|
+
end
|
|
77
|
+
|
|
61
78
|
def build_router(operations)
|
|
62
|
-
router = Hanami::Router.new
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
router = Hanami::Router.new.tap do |r|
|
|
80
|
+
operations.each do |operation|
|
|
81
|
+
normalized_path = operation.path.gsub('{', ':').gsub('}', '')
|
|
82
|
+
r.public_send(
|
|
83
|
+
operation.method,
|
|
84
|
+
normalized_path,
|
|
85
|
+
to: build_route(operation)
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
raise_error = @raise
|
|
90
|
+
Rack::Builder.app do
|
|
91
|
+
use BodyParserMiddleware, raise_error: raise_error
|
|
92
|
+
run router
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def build_route(operation)
|
|
97
|
+
lambda do |env|
|
|
98
|
+
env[OPERATION] = operation
|
|
99
|
+
env[PARAMETERS] = env['router.params']
|
|
100
|
+
env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH)
|
|
101
|
+
@app.call(env)
|
|
75
102
|
end
|
|
76
|
-
router
|
|
77
103
|
end
|
|
78
104
|
end
|
|
79
105
|
end
|
|
@@ -17,6 +17,7 @@ module OpenapiFirst
|
|
|
17
17
|
insert_property_defaults: true,
|
|
18
18
|
before_property_validation: proc do |data, property, property_schema, parent|
|
|
19
19
|
convert_nullable(data, property, property_schema, parent)
|
|
20
|
+
binary_format(data, property, property_schema, parent)
|
|
20
21
|
end
|
|
21
22
|
)
|
|
22
23
|
end
|
|
@@ -27,6 +28,14 @@ module OpenapiFirst
|
|
|
27
28
|
|
|
28
29
|
private
|
|
29
30
|
|
|
31
|
+
def binary_format(data, property, property_schema, _parent)
|
|
32
|
+
return unless property_schema.is_a?(Hash) && property_schema['format'] == 'binary'
|
|
33
|
+
|
|
34
|
+
property_schema['type'] = 'object'
|
|
35
|
+
property_schema.delete('format')
|
|
36
|
+
data[property].transform_keys!(&:to_s)
|
|
37
|
+
end
|
|
38
|
+
|
|
30
39
|
def convert_nullable(_data, _property, property_schema, _parent)
|
|
31
40
|
return unless property_schema.is_a?(Hash) && property_schema['nullable'] && property_schema['type']
|
|
32
41
|
|
|
@@ -11,9 +11,7 @@ module OpenapiFirst
|
|
|
11
11
|
def call(env)
|
|
12
12
|
return super if env.key?(OPERATION)
|
|
13
13
|
|
|
14
|
-
@router ||= Router.new(
|
|
15
|
-
super(e)
|
|
16
|
-
}, spec: @options.fetch(:spec), raise_error: @options.fetch(:raise_error, false))
|
|
14
|
+
@router ||= Router.new(->(e) { super(e) }, @options)
|
|
17
15
|
@router.call(env)
|
|
18
16
|
end
|
|
19
17
|
end
|
data/openapi_first.gemspec
CHANGED
|
@@ -32,11 +32,11 @@ Gem::Specification.new do |spec|
|
|
|
32
32
|
spec.bindir = 'exe'
|
|
33
33
|
spec.require_paths = ['lib']
|
|
34
34
|
|
|
35
|
-
spec.required_ruby_version = '>=
|
|
35
|
+
spec.required_ruby_version = '>= 3.0.5'
|
|
36
36
|
|
|
37
37
|
spec.add_runtime_dependency 'deep_merge', '>= 1.2.1'
|
|
38
|
-
spec.add_runtime_dependency 'hanami-router', '2.0.
|
|
39
|
-
spec.add_runtime_dependency 'hanami-utils', '2.0.
|
|
38
|
+
spec.add_runtime_dependency 'hanami-router', '~> 2.0.0'
|
|
39
|
+
spec.add_runtime_dependency 'hanami-utils', '~> 2.0.0'
|
|
40
40
|
spec.add_runtime_dependency 'json_refs', '~> 0.1', '>= 0.1.7'
|
|
41
41
|
spec.add_runtime_dependency 'json_schemer', '~> 0.2.16'
|
|
42
42
|
spec.add_runtime_dependency 'multi_json', '~> 1.14'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openapi_first
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.21.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andreas Haller
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2023-03-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: deep_merge
|
|
@@ -28,30 +28,30 @@ dependencies:
|
|
|
28
28
|
name: hanami-router
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
|
-
- -
|
|
31
|
+
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 2.0.
|
|
33
|
+
version: 2.0.0
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
|
-
- -
|
|
38
|
+
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 2.0.
|
|
40
|
+
version: 2.0.0
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: hanami-utils
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
|
-
- -
|
|
45
|
+
- - "~>"
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: 2.0.
|
|
47
|
+
version: 2.0.0
|
|
48
48
|
type: :runtime
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
|
-
- -
|
|
52
|
+
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: 2.0.
|
|
54
|
+
version: 2.0.0
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: json_refs
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -215,6 +215,7 @@ files:
|
|
|
215
215
|
- examples/openapi.yaml
|
|
216
216
|
- lib/openapi_first.rb
|
|
217
217
|
- lib/openapi_first/app.rb
|
|
218
|
+
- lib/openapi_first/body_parser_middleware.rb
|
|
218
219
|
- lib/openapi_first/coverage.rb
|
|
219
220
|
- lib/openapi_first/default_operation_resolver.rb
|
|
220
221
|
- lib/openapi_first/definition.rb
|
|
@@ -248,7 +249,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
248
249
|
requirements:
|
|
249
250
|
- - ">="
|
|
250
251
|
- !ruby/object:Gem::Version
|
|
251
|
-
version:
|
|
252
|
+
version: 3.0.5
|
|
252
253
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
253
254
|
requirements:
|
|
254
255
|
- - ">="
|