openapi_first 0.20.0 → 0.21.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
- - ">="
|