openapi_first 0.12.0.alpha2 → 0.12.4
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 +20 -0
- data/CHANGELOG.md +18 -1
- data/Gemfile.lock +18 -18
- data/README.md +21 -5
- data/benchmarks/Gemfile.lock +12 -12
- data/benchmarks/apps/openapi_first.ru +1 -1
- data/examples/app.rb +3 -2
- data/lib/openapi_first.rb +33 -7
- data/lib/openapi_first/app.rb +11 -4
- data/lib/openapi_first/operation.rb +23 -3
- data/lib/openapi_first/request_validation.rb +18 -12
- data/lib/openapi_first/response_validation.rb +13 -19
- data/lib/openapi_first/response_validator.rb +2 -46
- data/lib/openapi_first/router.rb +19 -29
- data/lib/openapi_first/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c55b490840191990eac078738a26002b09f794b0f555ae1144faa679fadd7697
|
|
4
|
+
data.tar.gz: 855132c7c8893b9c02670926c820273aa4b7a407a6e7d02dd99ad2f600dd714f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a1acd2cfa2197bcf9dd760cbf63e146d6ffcc5b9a4fef1b291a0d781c095a06976d02b40fae2b6393307b6f958f34b489514e7dcb22bb89df186c2c31c76f1fe
|
|
7
|
+
data.tar.gz: 8832ddbbc1101985806d0752f74ab2ca14cc7322def44cc0d3537dec3d04592dfce8b2c8d646d21b100e427176dc7a176c3472fca28fc85ed4ae881757a471d1
|
data/.rubocop.yml
CHANGED
|
@@ -14,6 +14,8 @@ Layout/SpaceAroundMethodCallOperator:
|
|
|
14
14
|
Enabled: true
|
|
15
15
|
Lint/DeprecatedOpenSSLConstant:
|
|
16
16
|
Enabled: true
|
|
17
|
+
Lint/DuplicateElsifCondition:
|
|
18
|
+
Enabled: true
|
|
17
19
|
Lint/RaiseException:
|
|
18
20
|
Enabled: true
|
|
19
21
|
Lint/MixedRegexpCaptureTypes:
|
|
@@ -28,7 +30,25 @@ Lint/StructNewOverride:
|
|
|
28
30
|
Enabled: true
|
|
29
31
|
Style/HashEachMethods:
|
|
30
32
|
Enabled: false
|
|
33
|
+
Style/AccessorGrouping:
|
|
34
|
+
Enabled: true
|
|
35
|
+
Style/ArrayCoercion:
|
|
36
|
+
Enabled: true
|
|
37
|
+
Style/BisectedAttrAccessor:
|
|
38
|
+
Enabled: true
|
|
39
|
+
Style/CaseLikeIf:
|
|
40
|
+
Enabled: true
|
|
41
|
+
Style/HashAsLastArrayItem:
|
|
42
|
+
Enabled: true
|
|
43
|
+
Style/HashLikeCase:
|
|
44
|
+
Enabled: true
|
|
31
45
|
Style/HashTransformKeys:
|
|
32
46
|
Enabled: true
|
|
33
47
|
Style/HashTransformValues:
|
|
34
48
|
Enabled: true
|
|
49
|
+
Style/RedundantAssignment:
|
|
50
|
+
Enabled: true
|
|
51
|
+
Style/RedundantFetchBlock:
|
|
52
|
+
Enabled: true
|
|
53
|
+
Style/RedundantFileExtensionInRequire:
|
|
54
|
+
Enabled: true
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.12.4
|
|
4
|
+
- content-type is found while ignoring additional content-type parameters (`application/json` is found when request/response content-type is `application/json; charset=UTF8`)
|
|
5
|
+
- Support wildcard mime-types when finding the content-type
|
|
6
|
+
|
|
7
|
+
## 0.12.3
|
|
8
|
+
- Add `response_validation:`, `router_raise_error` options to standalone mode.
|
|
9
|
+
|
|
10
|
+
## 0.12.2
|
|
11
|
+
- Allow response to have no media type object specified
|
|
12
|
+
|
|
13
|
+
## 0.12.1
|
|
14
|
+
- Fix response when handler returns 404 or 405
|
|
15
|
+
- Don't validate the response content if status is 205 (no content)
|
|
16
|
+
|
|
17
|
+
## 0.12.0
|
|
18
|
+
- Change `ResponseValidator` to raise an exception if it found a problem
|
|
19
|
+
- Params have symbolized keys now
|
|
20
|
+
- Remove `not_found` option from Router. Return 405 if HTTP verb is not allowed (via Hanami::Router)
|
|
4
21
|
- Add `raise_error` option to OpenapiFirst.app (false by default)
|
|
5
22
|
- Add ResponseValidation to OpenapiFirst.app if raise_error option is true
|
|
6
23
|
- Rename `raise` option to `raise_error`
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
openapi_first (0.12.
|
|
4
|
+
openapi_first (0.12.4)
|
|
5
5
|
deep_merge (>= 1.2.1)
|
|
6
6
|
hanami-router (~> 2.0.alpha3)
|
|
7
7
|
hanami-utils (~> 2.0.alpha1)
|
|
@@ -13,7 +13,7 @@ PATH
|
|
|
13
13
|
GEM
|
|
14
14
|
remote: https://rubygems.org/
|
|
15
15
|
specs:
|
|
16
|
-
activesupport (6.0.3.
|
|
16
|
+
activesupport (6.0.3.2)
|
|
17
17
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
18
18
|
i18n (>= 0.7, < 2)
|
|
19
19
|
minitest (~> 5.1)
|
|
@@ -21,12 +21,12 @@ GEM
|
|
|
21
21
|
zeitwerk (~> 2.2, >= 2.2.2)
|
|
22
22
|
addressable (2.7.0)
|
|
23
23
|
public_suffix (>= 2.0.2, < 5.0)
|
|
24
|
-
ast (2.4.
|
|
24
|
+
ast (2.4.1)
|
|
25
25
|
builder (3.2.4)
|
|
26
26
|
coderay (1.1.3)
|
|
27
27
|
concurrent-ruby (1.1.6)
|
|
28
28
|
deep_merge (1.2.1)
|
|
29
|
-
diff-lcs (1.
|
|
29
|
+
diff-lcs (1.4.4)
|
|
30
30
|
ecma-re-validator (0.2.1)
|
|
31
31
|
regexp_parser (~> 1.2)
|
|
32
32
|
hana (1.3.6)
|
|
@@ -39,9 +39,9 @@ GEM
|
|
|
39
39
|
transproc (~> 1.0)
|
|
40
40
|
hansi (0.2.0)
|
|
41
41
|
hash-deep-merge (0.1.1)
|
|
42
|
-
i18n (1.8.
|
|
42
|
+
i18n (1.8.4)
|
|
43
43
|
concurrent-ruby (~> 1.0)
|
|
44
|
-
json_schemer (0.2.
|
|
44
|
+
json_schemer (0.2.12)
|
|
45
45
|
ecma-re-validator (~> 0.2)
|
|
46
46
|
hana (~> 1.3)
|
|
47
47
|
regexp_parser (~> 1.5)
|
|
@@ -49,13 +49,13 @@ GEM
|
|
|
49
49
|
method_source (1.0.0)
|
|
50
50
|
mini_portile2 (2.4.0)
|
|
51
51
|
minitest (5.14.1)
|
|
52
|
-
multi_json (1.
|
|
52
|
+
multi_json (1.15.0)
|
|
53
53
|
mustermann (1.1.1)
|
|
54
54
|
ruby2_keywords (~> 0.0.1)
|
|
55
55
|
mustermann-contrib (1.1.1)
|
|
56
56
|
hansi (~> 0.2.0)
|
|
57
57
|
mustermann (= 1.1.1)
|
|
58
|
-
nokogiri (1.10.
|
|
58
|
+
nokogiri (1.10.10)
|
|
59
59
|
mini_portile2 (~> 2.4.0)
|
|
60
60
|
oas_parser (0.25.1)
|
|
61
61
|
activesupport (>= 4.0.0)
|
|
@@ -65,19 +65,19 @@ GEM
|
|
|
65
65
|
hash-deep-merge
|
|
66
66
|
mustermann-contrib (~> 1.1.1)
|
|
67
67
|
nokogiri
|
|
68
|
-
parallel (1.19.
|
|
69
|
-
parser (2.7.1.
|
|
70
|
-
ast (~> 2.4.
|
|
68
|
+
parallel (1.19.2)
|
|
69
|
+
parser (2.7.1.4)
|
|
70
|
+
ast (~> 2.4.1)
|
|
71
71
|
pry (0.13.1)
|
|
72
72
|
coderay (~> 1.1)
|
|
73
73
|
method_source (~> 1.0)
|
|
74
74
|
public_suffix (4.0.5)
|
|
75
|
-
rack (2.2.
|
|
75
|
+
rack (2.2.3)
|
|
76
76
|
rack-test (1.1.0)
|
|
77
77
|
rack (>= 1.0, < 3)
|
|
78
78
|
rainbow (3.0.0)
|
|
79
79
|
rake (13.0.1)
|
|
80
|
-
regexp_parser (1.7.
|
|
80
|
+
regexp_parser (1.7.1)
|
|
81
81
|
rexml (3.2.4)
|
|
82
82
|
rspec (3.9.0)
|
|
83
83
|
rspec-core (~> 3.9.0)
|
|
@@ -92,16 +92,16 @@ GEM
|
|
|
92
92
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
93
93
|
rspec-support (~> 3.9.0)
|
|
94
94
|
rspec-support (3.9.3)
|
|
95
|
-
rubocop (0.
|
|
95
|
+
rubocop (0.88.0)
|
|
96
96
|
parallel (~> 1.10)
|
|
97
|
-
parser (>= 2.7.
|
|
97
|
+
parser (>= 2.7.1.1)
|
|
98
98
|
rainbow (>= 2.2.2, < 4.0)
|
|
99
99
|
regexp_parser (>= 1.7)
|
|
100
100
|
rexml
|
|
101
|
-
rubocop-ast (>= 0.0.
|
|
101
|
+
rubocop-ast (>= 0.1.0, < 1.0)
|
|
102
102
|
ruby-progressbar (~> 1.7)
|
|
103
103
|
unicode-display_width (>= 1.4.0, < 2.0)
|
|
104
|
-
rubocop-ast (0.0
|
|
104
|
+
rubocop-ast (0.2.0)
|
|
105
105
|
parser (>= 2.7.0.1)
|
|
106
106
|
ruby-progressbar (1.10.1)
|
|
107
107
|
ruby2_keywords (0.0.2)
|
|
@@ -111,7 +111,7 @@ GEM
|
|
|
111
111
|
thread_safe (~> 0.1)
|
|
112
112
|
unicode-display_width (1.7.0)
|
|
113
113
|
uri_template (0.7.0)
|
|
114
|
-
zeitwerk (2.
|
|
114
|
+
zeitwerk (2.4.0)
|
|
115
115
|
|
|
116
116
|
PLATFORMS
|
|
117
117
|
ruby
|
data/README.md
CHANGED
|
@@ -36,7 +36,6 @@ Options and their defaults:
|
|
|
36
36
|
| Name | Possible values | Description | Default
|
|
37
37
|
|:---|---|---|---|
|
|
38
38
|
|`spec:`| | The spec loaded via `OpenapiFirst.load` ||
|
|
39
|
-
| `not_found:` |`nil`, `:continue`, `Proc`| Specifies what to do if the path was not found in the API description. `nil` (default) returns a 404 response. `:continue` does nothing an calls the next app. `Proc` (or something that responds to `call`) to customize the response. | `nil` (return 404)
|
|
40
39
|
| `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)
|
|
41
40
|
|
|
42
41
|
## OpenapiFirst::RequestValidation
|
|
@@ -143,7 +142,7 @@ Instead of composing these middlewares yourself you can use `OpenapiFirst.app`.
|
|
|
143
142
|
module Pets
|
|
144
143
|
def self.find_pet(params, res)
|
|
145
144
|
{
|
|
146
|
-
id: params[
|
|
145
|
+
id: params[:id],
|
|
147
146
|
name: 'Oscar'
|
|
148
147
|
}
|
|
149
148
|
end
|
|
@@ -151,7 +150,12 @@ end
|
|
|
151
150
|
|
|
152
151
|
# In config.ru:
|
|
153
152
|
require 'openapi_first'
|
|
154
|
-
run OpenapiFirst.app(
|
|
153
|
+
run OpenapiFirst.app(
|
|
154
|
+
'./openapi/openapi.yaml',
|
|
155
|
+
namespace: Pets,
|
|
156
|
+
response_validation: ENV['RACK_ENV'] == 'test',
|
|
157
|
+
router_raise_error: ENV['RACK_ENV'] == 'test'
|
|
158
|
+
)
|
|
155
159
|
```
|
|
156
160
|
|
|
157
161
|
The above will use the mentioned Rack middlewares to:
|
|
@@ -160,6 +164,17 @@ The above will use the mentioned Rack middlewares to:
|
|
|
160
164
|
- Map the request to a method call `Pets.find_pet` based on the `operationId` in the API description
|
|
161
165
|
- Set the response content type according to your spec (here with the default status code `200`)
|
|
162
166
|
|
|
167
|
+
### Options and their defaults:
|
|
168
|
+
|
|
169
|
+
| Name | Possible values | Description | Default
|
|
170
|
+
|:---|---|---|---|
|
|
171
|
+
| `spec_path` || A filepath to an OpenAPI definition file. |
|
|
172
|
+
| `namespace:` || A class or module where to find the handler methods.|
|
|
173
|
+
| `response_validation:` | `true`, `false` | If set to true it raises an exception if the response is invalid. This is useful during testing. | `false`
|
|
174
|
+
| `router_raise_error:` | `true`, `false` | If set to true it raises an exception (subclass of `OpenapiFirst::Error` when a request path/method is not specified. This is useful during testing. | `false`
|
|
175
|
+
| `request_validation_raise_error:` | `true`, `false` | If set to true it raises an exception (subclass of `OpenapiFirst::Error` when a request is not valid. | `false`
|
|
176
|
+
|
|
177
|
+
|
|
163
178
|
Handler functions (`find_pet`) are called with two arguments:
|
|
164
179
|
|
|
165
180
|
- `params` - Holds the parsed request body, filtered query params and path parameters
|
|
@@ -190,7 +205,7 @@ OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
|
|
|
190
205
|
|
|
191
206
|
## Manual response validation
|
|
192
207
|
|
|
193
|
-
|
|
208
|
+
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.
|
|
194
209
|
|
|
195
210
|
```ruby
|
|
196
211
|
# In your test (rspec example):
|
|
@@ -198,7 +213,8 @@ require 'openapi_first'
|
|
|
198
213
|
spec = OpenapiFirst.load('petstore.yaml')
|
|
199
214
|
validator = OpenapiFirst::ResponseValidator.new(spec)
|
|
200
215
|
|
|
201
|
-
|
|
216
|
+
# This will raise an exception if it found an error
|
|
217
|
+
validator.validate(last_request, last_response)
|
|
202
218
|
```
|
|
203
219
|
|
|
204
220
|
## Handling only certain paths
|
data/benchmarks/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
openapi_first (0.12.
|
|
4
|
+
openapi_first (0.12.4)
|
|
5
5
|
deep_merge (>= 1.2.1)
|
|
6
6
|
hanami-router (~> 2.0.alpha3)
|
|
7
7
|
hanami-utils (~> 2.0.alpha1)
|
|
@@ -13,7 +13,7 @@ PATH
|
|
|
13
13
|
GEM
|
|
14
14
|
remote: https://rubygems.org/
|
|
15
15
|
specs:
|
|
16
|
-
activesupport (6.0.3.
|
|
16
|
+
activesupport (6.0.3.2)
|
|
17
17
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
18
18
|
i18n (>= 0.7, < 2)
|
|
19
19
|
minitest (~> 5.1)
|
|
@@ -25,13 +25,13 @@ GEM
|
|
|
25
25
|
benchmark-memory (0.1.2)
|
|
26
26
|
memory_profiler (~> 0.9)
|
|
27
27
|
builder (3.2.4)
|
|
28
|
-
committee (4.
|
|
28
|
+
committee (4.1.0)
|
|
29
29
|
json_schema (~> 0.14, >= 0.14.3)
|
|
30
30
|
openapi_parser (>= 0.11.1)
|
|
31
31
|
rack (>= 1.5)
|
|
32
32
|
concurrent-ruby (1.1.6)
|
|
33
33
|
deep_merge (1.2.1)
|
|
34
|
-
dry-configurable (0.11.
|
|
34
|
+
dry-configurable (0.11.6)
|
|
35
35
|
concurrent-ruby (~> 1.0)
|
|
36
36
|
dry-core (~> 0.4, >= 0.4.7)
|
|
37
37
|
dry-equalizer (~> 0.2)
|
|
@@ -55,7 +55,7 @@ GEM
|
|
|
55
55
|
dry-logic (~> 1.0, >= 1.0.2)
|
|
56
56
|
ecma-re-validator (0.2.1)
|
|
57
57
|
regexp_parser (~> 1.2)
|
|
58
|
-
grape (1.
|
|
58
|
+
grape (1.4.0)
|
|
59
59
|
activesupport
|
|
60
60
|
builder
|
|
61
61
|
dry-types (>= 1.1)
|
|
@@ -72,10 +72,10 @@ GEM
|
|
|
72
72
|
transproc (~> 1.0)
|
|
73
73
|
hansi (0.2.0)
|
|
74
74
|
hash-deep-merge (0.1.1)
|
|
75
|
-
i18n (1.8.
|
|
75
|
+
i18n (1.8.4)
|
|
76
76
|
concurrent-ruby (~> 1.0)
|
|
77
|
-
json_schema (0.20.
|
|
78
|
-
json_schemer (0.2.
|
|
77
|
+
json_schema (0.20.9)
|
|
78
|
+
json_schemer (0.2.12)
|
|
79
79
|
ecma-re-validator (~> 0.2)
|
|
80
80
|
hana (~> 1.3)
|
|
81
81
|
regexp_parser (~> 1.5)
|
|
@@ -83,7 +83,7 @@ GEM
|
|
|
83
83
|
memory_profiler (0.9.14)
|
|
84
84
|
mini_portile2 (2.4.0)
|
|
85
85
|
minitest (5.14.1)
|
|
86
|
-
multi_json (1.
|
|
86
|
+
multi_json (1.15.0)
|
|
87
87
|
mustermann (1.1.1)
|
|
88
88
|
ruby2_keywords (~> 0.0.1)
|
|
89
89
|
mustermann-contrib (1.1.1)
|
|
@@ -91,7 +91,7 @@ GEM
|
|
|
91
91
|
mustermann (= 1.1.1)
|
|
92
92
|
mustermann-grape (1.0.1)
|
|
93
93
|
mustermann (>= 1.0.0)
|
|
94
|
-
nokogiri (1.10.
|
|
94
|
+
nokogiri (1.10.10)
|
|
95
95
|
mini_portile2 (~> 2.4.0)
|
|
96
96
|
oas_parser (0.25.1)
|
|
97
97
|
activesupport (>= 4.0.0)
|
|
@@ -103,7 +103,7 @@ GEM
|
|
|
103
103
|
nokogiri
|
|
104
104
|
openapi_parser (0.11.2)
|
|
105
105
|
public_suffix (4.0.5)
|
|
106
|
-
rack (2.2.
|
|
106
|
+
rack (2.2.3)
|
|
107
107
|
rack-accept (0.4.5)
|
|
108
108
|
rack (>= 0.4)
|
|
109
109
|
rack-protection (2.0.8.1)
|
|
@@ -125,7 +125,7 @@ GEM
|
|
|
125
125
|
tzinfo (1.2.7)
|
|
126
126
|
thread_safe (~> 0.1)
|
|
127
127
|
uri_template (0.7.0)
|
|
128
|
-
zeitwerk (2.
|
|
128
|
+
zeitwerk (2.4.0)
|
|
129
129
|
|
|
130
130
|
PLATFORMS
|
|
131
131
|
ruby
|
data/examples/app.rb
CHANGED
|
@@ -13,9 +13,10 @@ module Web
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
oas_path = File.absolute_path('./openapi.yaml', __dir__)
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
App = OpenapiFirst.app(
|
|
18
18
|
oas_path,
|
|
19
19
|
namespace: Web,
|
|
20
|
-
|
|
20
|
+
router_raise_error: OpenapiFirst.env == 'test',
|
|
21
|
+
response_validation: OpenapiFirst.env == 'test'
|
|
21
22
|
)
|
data/lib/openapi_first.rb
CHANGED
|
@@ -31,14 +31,39 @@ module OpenapiFirst
|
|
|
31
31
|
Definition.new(parsed)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
def self.app(
|
|
34
|
+
def self.app(
|
|
35
|
+
spec,
|
|
36
|
+
namespace:,
|
|
37
|
+
router_raise_error: false,
|
|
38
|
+
request_validation_raise_error: false,
|
|
39
|
+
response_validation: false
|
|
40
|
+
)
|
|
35
41
|
spec = OpenapiFirst.load(spec) if spec.is_a?(String)
|
|
36
|
-
App.new(
|
|
42
|
+
App.new(
|
|
43
|
+
nil,
|
|
44
|
+
spec,
|
|
45
|
+
namespace: namespace,
|
|
46
|
+
router_raise_error: router_raise_error,
|
|
47
|
+
request_validation_raise_error: request_validation_raise_error,
|
|
48
|
+
response_validation: response_validation
|
|
49
|
+
)
|
|
37
50
|
end
|
|
38
51
|
|
|
39
|
-
def self.middleware(
|
|
52
|
+
def self.middleware(
|
|
53
|
+
spec,
|
|
54
|
+
namespace:,
|
|
55
|
+
router_raise_error: false,
|
|
56
|
+
request_validation_raise_error: false,
|
|
57
|
+
response_validation: false
|
|
58
|
+
)
|
|
40
59
|
spec = OpenapiFirst.load(spec) if spec.is_a?(String)
|
|
41
|
-
AppWithOptions.new(
|
|
60
|
+
AppWithOptions.new(
|
|
61
|
+
spec,
|
|
62
|
+
namespace: namespace,
|
|
63
|
+
router_raise_error: router_raise_error,
|
|
64
|
+
request_validation_raise_error: request_validation_raise_error,
|
|
65
|
+
response_validation: response_validation
|
|
66
|
+
)
|
|
42
67
|
end
|
|
43
68
|
|
|
44
69
|
class AppWithOptions
|
|
@@ -55,9 +80,10 @@ module OpenapiFirst
|
|
|
55
80
|
class Error < StandardError; end
|
|
56
81
|
class NotFoundError < Error; end
|
|
57
82
|
class NotImplementedError < RuntimeError; end
|
|
58
|
-
class
|
|
59
|
-
class
|
|
60
|
-
class
|
|
83
|
+
class ResponseInvalid < Error; end
|
|
84
|
+
class ResponseCodeNotFoundError < ResponseInvalid; end
|
|
85
|
+
class ResponseContentTypeNotFoundError < ResponseInvalid; end
|
|
86
|
+
class ResponseBodyInvalidError < ResponseInvalid; end
|
|
61
87
|
|
|
62
88
|
class RequestInvalidError < Error
|
|
63
89
|
def initialize(serialized_errors)
|
data/lib/openapi_first/app.rb
CHANGED
|
@@ -5,12 +5,19 @@ require 'logger'
|
|
|
5
5
|
|
|
6
6
|
module OpenapiFirst
|
|
7
7
|
class App
|
|
8
|
-
def initialize(
|
|
8
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
9
|
+
parent_app,
|
|
10
|
+
spec,
|
|
11
|
+
namespace:,
|
|
12
|
+
router_raise_error: false,
|
|
13
|
+
request_validation_raise_error: false,
|
|
14
|
+
response_validation: false
|
|
15
|
+
)
|
|
9
16
|
@stack = Rack::Builder.app do
|
|
10
17
|
freeze_app
|
|
11
|
-
use OpenapiFirst::Router, spec: spec, raise_error:
|
|
12
|
-
use OpenapiFirst::RequestValidation, raise_error:
|
|
13
|
-
use OpenapiFirst::ResponseValidation if
|
|
18
|
+
use OpenapiFirst::Router, spec: spec, raise_error: router_raise_error, parent_app: parent_app
|
|
19
|
+
use OpenapiFirst::RequestValidation, raise_error: request_validation_raise_error
|
|
20
|
+
use OpenapiFirst::ResponseValidation if response_validation
|
|
14
21
|
run OpenapiFirst::Responder.new(
|
|
15
22
|
spec: spec,
|
|
16
23
|
namespace: namespace
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'forwardable'
|
|
4
|
+
require 'json_schemer'
|
|
4
5
|
require_relative 'utils'
|
|
5
6
|
require_relative 'response_object'
|
|
6
7
|
|
|
@@ -25,6 +26,10 @@ module OpenapiFirst
|
|
|
25
26
|
@parameters_json_schema ||= build_parameters_json_schema
|
|
26
27
|
end
|
|
27
28
|
|
|
29
|
+
def parameters_schema
|
|
30
|
+
@parameters_schema ||= parameters_json_schema && JSONSchemer.schema(parameters_json_schema)
|
|
31
|
+
end
|
|
32
|
+
|
|
28
33
|
def content_type_for(status)
|
|
29
34
|
content = response_for(status)['content']
|
|
30
35
|
content.keys[0] if content
|
|
@@ -34,14 +39,22 @@ module OpenapiFirst
|
|
|
34
39
|
content = response_for(status)['content']
|
|
35
40
|
return if content.nil? || content.empty?
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
raise ResponseInvalid, "Response has no content-type for '#{name}'" unless content_type
|
|
43
|
+
|
|
44
|
+
media_type = find_content_for_content_type(content, content_type)
|
|
38
45
|
unless media_type
|
|
39
|
-
message = "Response content type not found
|
|
40
|
-
raise
|
|
46
|
+
message = "Response content type not found '#{content_type}' for '#{name}'"
|
|
47
|
+
raise ResponseContentTypeNotFoundError, message
|
|
41
48
|
end
|
|
42
49
|
media_type['schema']
|
|
43
50
|
end
|
|
44
51
|
|
|
52
|
+
def request_body_schema_for(request_content_type)
|
|
53
|
+
content = @operation.request_body.content
|
|
54
|
+
media_type = find_content_for_content_type(content, request_content_type)
|
|
55
|
+
media_type&.fetch('schema', nil)
|
|
56
|
+
end
|
|
57
|
+
|
|
45
58
|
def response_for(status)
|
|
46
59
|
@operation.response_by_code(status.to_s, use_default: true).raw
|
|
47
60
|
rescue OasParser::ResponseCodeNotFound
|
|
@@ -55,6 +68,13 @@ module OpenapiFirst
|
|
|
55
68
|
|
|
56
69
|
private
|
|
57
70
|
|
|
71
|
+
def find_content_for_content_type(content, request_content_type)
|
|
72
|
+
content.fetch(request_content_type) do |_|
|
|
73
|
+
type = request_content_type.split(';')[0]
|
|
74
|
+
content[type] || content["#{type.split('/')[0]}/*"] || content['*/*']
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
58
78
|
def build_parameters_json_schema
|
|
59
79
|
return unless @operation.parameters&.any?
|
|
60
80
|
|
|
@@ -50,32 +50,32 @@ module OpenapiFirst
|
|
|
50
50
|
|
|
51
51
|
parsed_request_body = parse_request_body!(body)
|
|
52
52
|
errors = validate_json_schema(schema, parsed_request_body)
|
|
53
|
-
|
|
53
|
+
halt_with_error(400, serialize_request_body_errors(errors)) if errors.any?
|
|
54
54
|
env[INBOX].merge! env[REQUEST_BODY] = parsed_request_body
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def parse_request_body!(body)
|
|
58
|
-
MultiJson.load(body)
|
|
58
|
+
MultiJson.load(body, symbolize_keys: true)
|
|
59
59
|
rescue MultiJson::ParseError => e
|
|
60
60
|
err = { title: 'Failed to parse body as JSON' }
|
|
61
61
|
err[:detail] = e.cause unless ENV['RACK_ENV'] == 'production'
|
|
62
|
-
|
|
62
|
+
halt_with_error(400, [err])
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def validate_request_content_type!(content_type, operation)
|
|
66
66
|
return if operation.request_body.content[content_type]
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
halt_with_error(415)
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def validate_request_body_presence!(body, operation)
|
|
72
72
|
return unless operation.request_body.required && body.empty?
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
halt_with_error(415, 'Request body is required')
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def validate_json_schema(schema, object)
|
|
78
|
-
|
|
78
|
+
schema.validate(Utils.deep_stringify(object))
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
|
|
@@ -85,10 +85,10 @@ module OpenapiFirst
|
|
|
85
85
|
}
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
-
def
|
|
88
|
+
def halt_with_error(status, errors = [default_error(status)])
|
|
89
89
|
raise RequestInvalidError, errors if @raise
|
|
90
90
|
|
|
91
|
-
Rack::Response.new(
|
|
91
|
+
halt Rack::Response.new(
|
|
92
92
|
MultiJson.dump(errors: errors),
|
|
93
93
|
status,
|
|
94
94
|
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
|
@@ -98,7 +98,9 @@ module OpenapiFirst
|
|
|
98
98
|
def request_body_schema(content_type, operation)
|
|
99
99
|
return unless operation
|
|
100
100
|
|
|
101
|
-
operation.
|
|
101
|
+
schema = operation.request_body_schema_for(content_type)
|
|
102
|
+
|
|
103
|
+
JSONSchemer.schema(schema) if schema
|
|
102
104
|
end
|
|
103
105
|
|
|
104
106
|
def serialize_request_body_errors(validation_errors)
|
|
@@ -116,8 +118,11 @@ module OpenapiFirst
|
|
|
116
118
|
return unless json_schema
|
|
117
119
|
|
|
118
120
|
params = filtered_params(json_schema, params)
|
|
119
|
-
errors =
|
|
120
|
-
|
|
121
|
+
errors = validate_json_schema(
|
|
122
|
+
operation.parameters_schema,
|
|
123
|
+
params
|
|
124
|
+
)
|
|
125
|
+
halt_with_error(400, serialize_query_parameter_errors(errors)) if errors.any?
|
|
121
126
|
env[PARAMETERS] = params
|
|
122
127
|
env[INBOX].merge! params
|
|
123
128
|
end
|
|
@@ -125,7 +130,8 @@ module OpenapiFirst
|
|
|
125
130
|
def filtered_params(json_schema, params)
|
|
126
131
|
json_schema['properties']
|
|
127
132
|
.each_with_object({}) do |key_value, result|
|
|
128
|
-
parameter_name
|
|
133
|
+
parameter_name = key_value[0].to_sym
|
|
134
|
+
schema = key_value[1]
|
|
129
135
|
next unless params.key?(parameter_name)
|
|
130
136
|
|
|
131
137
|
value = params[parameter_name]
|
|
@@ -17,30 +17,24 @@ module OpenapiFirst
|
|
|
17
17
|
operation = env[OPERATION]
|
|
18
18
|
return @app.call(env) unless operation
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
response = @app.call(env)
|
|
21
|
+
validate(response, operation)
|
|
22
|
+
response
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate(response, operation)
|
|
26
|
+
status, headers, body = response.to_a
|
|
27
|
+
return validate_status_only(operation, status) if status == 204
|
|
28
|
+
|
|
21
29
|
content_type = headers[Rack::CONTENT_TYPE]
|
|
22
30
|
response_schema = operation.response_schema_for(status, content_type)
|
|
23
31
|
validate_response_body(response_schema, body) if response_schema
|
|
24
|
-
|
|
25
|
-
[status, headers, body]
|
|
26
32
|
end
|
|
27
33
|
|
|
28
34
|
private
|
|
29
35
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def error(message)
|
|
35
|
-
{ title: message }
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def error_response(status, errors)
|
|
39
|
-
Rack::Response.new(
|
|
40
|
-
MultiJson.dump(errors: errors),
|
|
41
|
-
status,
|
|
42
|
-
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
|
43
|
-
).finish
|
|
36
|
+
def validate_status_only(operation, status)
|
|
37
|
+
operation.response_for(status)
|
|
44
38
|
end
|
|
45
39
|
|
|
46
40
|
def validate_response_body(schema, response)
|
|
@@ -48,7 +42,7 @@ module OpenapiFirst
|
|
|
48
42
|
response.each { |chunk| full_body << chunk }
|
|
49
43
|
data = full_body.empty? ? {} : load_json(full_body)
|
|
50
44
|
errors = JSONSchemer.schema(schema).validate(data).to_a.map do |error|
|
|
51
|
-
|
|
45
|
+
error_message_for(error)
|
|
52
46
|
end
|
|
53
47
|
raise ResponseBodyInvalidError, errors.join(', ') if errors.any?
|
|
54
48
|
end
|
|
@@ -59,7 +53,7 @@ module OpenapiFirst
|
|
|
59
53
|
string
|
|
60
54
|
end
|
|
61
55
|
|
|
62
|
-
def
|
|
56
|
+
def error_message_for(error)
|
|
63
57
|
err = ValidationFormat.error_details(error)
|
|
64
58
|
[err[:title], error['data_pointer'], err[:detail]].compact.join(' ')
|
|
65
59
|
end
|
|
@@ -10,57 +10,13 @@ module OpenapiFirst
|
|
|
10
10
|
def initialize(spec)
|
|
11
11
|
@spec = spec
|
|
12
12
|
@router = Router.new(->(_env) {}, spec: spec, raise_error: true)
|
|
13
|
+
@response_validation = ResponseValidation.new(->(response) { response.to_a })
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def validate(request, response)
|
|
16
|
-
errors = validation_errors(request, response)
|
|
17
|
-
Validation.new(errors || [])
|
|
18
|
-
rescue OpenapiFirst::ResponseCodeNotFoundError, OpenapiFirst::NotFoundError => e
|
|
19
|
-
Validation.new([e.message])
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def validate_operation(request, response)
|
|
23
|
-
errors = validation_errors(request, response)
|
|
24
|
-
Validation.new(errors || [])
|
|
25
|
-
rescue OpenapiFirst::ResponseCodeNotFoundError, OpenapiFirst::NotFoundError => e
|
|
26
|
-
Validation.new([e.message])
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
def validation_errors(request, response)
|
|
32
|
-
content = response_for(request, response)&.fetch('content', nil)
|
|
33
|
-
return unless content
|
|
34
|
-
|
|
35
|
-
content_type = content[response.content_type]
|
|
36
|
-
return ["Content type not found: '#{response.content_type}'"] unless content_type
|
|
37
|
-
|
|
38
|
-
response_schema = content_type['schema']
|
|
39
|
-
return unless response_schema
|
|
40
|
-
|
|
41
|
-
response_data = MultiJson.load(response.body)
|
|
42
|
-
validate_json_schema(response_schema, response_data)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def validate_json_schema(schema, data)
|
|
46
|
-
JSONSchemer.schema(schema).validate(data).to_a.map do |error|
|
|
47
|
-
format_error(error)
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def format_error(error)
|
|
52
|
-
ValidationFormat.error_details(error)
|
|
53
|
-
.merge!(
|
|
54
|
-
data_pointer: error['data_pointer'],
|
|
55
|
-
schema_pointer: error['schema_pointer']
|
|
56
|
-
)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def response_for(request, response)
|
|
60
17
|
env = request.env.dup
|
|
61
18
|
@router.call(env)
|
|
62
|
-
|
|
63
|
-
operation&.response_for(response.status)
|
|
19
|
+
@response_validation.validate(response, env[OPERATION])
|
|
64
20
|
end
|
|
65
21
|
end
|
|
66
22
|
end
|
data/lib/openapi_first/router.rb
CHANGED
|
@@ -6,58 +6,47 @@ require_relative 'utils'
|
|
|
6
6
|
|
|
7
7
|
module OpenapiFirst
|
|
8
8
|
class Router
|
|
9
|
-
NOT_FOUND = Rack::Response.new('', 404).finish.freeze
|
|
10
|
-
DEFAULT_NOT_FOUND_APP = ->(_env) { NOT_FOUND }
|
|
11
|
-
|
|
12
9
|
def initialize(
|
|
13
10
|
app,
|
|
14
11
|
spec:,
|
|
15
12
|
raise_error: false,
|
|
16
|
-
parent_app: nil
|
|
17
|
-
not_found: nil
|
|
13
|
+
parent_app: nil
|
|
18
14
|
)
|
|
19
15
|
@app = app
|
|
20
16
|
@parent_app = parent_app
|
|
21
17
|
@raise = raise_error
|
|
22
|
-
@failure_app = find_failure_app(not_found)
|
|
23
|
-
if @failure_app.nil?
|
|
24
|
-
raise ArgumentError,
|
|
25
|
-
'not_found must be nil, :continue or must respond to call'
|
|
26
|
-
end
|
|
27
18
|
@filepath = spec.filepath
|
|
28
19
|
@router = build_router(spec.operations)
|
|
29
20
|
end
|
|
30
21
|
|
|
31
22
|
def call(env)
|
|
32
23
|
env[OPERATION] = nil
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
response = call_router(env)
|
|
25
|
+
if env[OPERATION].nil?
|
|
26
|
+
return @parent_app.call(env) if @parent_app # This should only happen if used via OpenapiFirst.middlware
|
|
35
27
|
|
|
36
|
-
|
|
37
|
-
req = Rack::Request.new(env)
|
|
38
|
-
msg = "Could not find definition for #{req.request_method} '#{req.path}' in API description #{@filepath}"
|
|
39
|
-
raise NotFoundError, msg
|
|
28
|
+
raise_error(env) if @raise
|
|
40
29
|
end
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@failure_app.call(env)
|
|
30
|
+
response
|
|
44
31
|
end
|
|
45
32
|
|
|
46
|
-
|
|
33
|
+
ORIGINAL_PATH = 'openapi_first.path_info'
|
|
47
34
|
|
|
48
|
-
|
|
49
|
-
return DEFAULT_NOT_FOUND_APP if option.nil?
|
|
50
|
-
return @app if option == :continue
|
|
35
|
+
private
|
|
51
36
|
|
|
52
|
-
|
|
37
|
+
def raise_error(env)
|
|
38
|
+
req = Rack::Request.new(env)
|
|
39
|
+
msg = "Could not find definition for #{req.request_method} '#{req.path}' in API description #{@filepath}"
|
|
40
|
+
raise NotFoundError, msg
|
|
53
41
|
end
|
|
54
42
|
|
|
55
|
-
def
|
|
56
|
-
|
|
43
|
+
def call_router(env)
|
|
44
|
+
# Changing and restoring PATH_INFO is needed, because Hanami::Router does not respect existing script_path
|
|
45
|
+
env[ORIGINAL_PATH] = env[Rack::PATH_INFO]
|
|
57
46
|
env[Rack::PATH_INFO] = Rack::Request.new(env).path
|
|
58
|
-
@router.
|
|
47
|
+
@router.call(env)
|
|
59
48
|
ensure
|
|
60
|
-
env[Rack::PATH_INFO] =
|
|
49
|
+
env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
|
|
61
50
|
end
|
|
62
51
|
|
|
63
52
|
def build_router(operations) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
@@ -73,7 +62,8 @@ module OpenapiFirst
|
|
|
73
62
|
normalized_path,
|
|
74
63
|
to: lambda do |env|
|
|
75
64
|
env[OPERATION] = operation
|
|
76
|
-
env[PARAMETERS] =
|
|
65
|
+
env[PARAMETERS] = env['router.params']
|
|
66
|
+
env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH)
|
|
77
67
|
@app.call(env)
|
|
78
68
|
end
|
|
79
69
|
)
|
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.12.
|
|
4
|
+
version: 0.12.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andreas Haller
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2020-
|
|
11
|
+
date: 2020-07-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: deep_merge
|
|
@@ -236,9 +236,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
236
236
|
version: '0'
|
|
237
237
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
238
238
|
requirements:
|
|
239
|
-
- - "
|
|
239
|
+
- - ">="
|
|
240
240
|
- !ruby/object:Gem::Version
|
|
241
|
-
version:
|
|
241
|
+
version: '0'
|
|
242
242
|
requirements: []
|
|
243
243
|
rubygems_version: 3.1.2
|
|
244
244
|
signing_key:
|