openapi_first 0.21.0 → 1.0.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -1
- data/Gemfile.lock +15 -12
- data/README.md +22 -172
- data/benchmarks/Gemfile.lock +16 -15
- data/benchmarks/apps/openapi_first.ru +1 -1
- data/examples/app.rb +12 -16
- data/lib/openapi_first/operation.rb +18 -70
- data/lib/openapi_first/request_validation.rb +31 -76
- data/lib/openapi_first/router.rb +9 -5
- data/lib/openapi_first/utils.rb +11 -5
- data/lib/openapi_first/version.rb +1 -1
- data/lib/openapi_first.rb +3 -35
- data/openapi_first.gemspec +3 -1
- metadata +46 -14
- data/lib/openapi_first/app.rb +0 -29
- data/lib/openapi_first/coverage.rb +0 -28
- data/lib/openapi_first/default_operation_resolver.rb +0 -63
- data/lib/openapi_first/inbox.rb +0 -13
- data/lib/openapi_first/rack_responder.rb +0 -12
- data/lib/openapi_first/responder.rb +0 -44
- data/lib/openapi_first/response_object.rb +0 -20
- data/lib/openapi_first/validation.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2248133fb2e0b761fb314629dcbfa7c9a6b7e0ac03b59887fe158968b17ed827
|
4
|
+
data.tar.gz: b0012d8af3c9dee1fa94334be7890e260f0173946129b53ec8f930f768f8de28
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c615c847efcd10cfd145bcc99ede55da2eaa76fe91b9d0ab3fdb159c20b6cc314309f583ba0b784c3e3493a7a534ed1836309807c549bd8c40b226f2f9bf77e9
|
7
|
+
data.tar.gz: 83c476b9b67ce15ad71e052af8360e013659da13fbbd8f86a55bae9d4bbcf08f4ac291187dfccc5786e957c65d2a3124366f007998c675b4a3e29bccaecf332a
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,16 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
##
|
3
|
+
## 1.0.0.beta1
|
4
|
+
- Removed: `OpenapiFirst::Responder` and `OpenapiFirst::RackResponder`
|
5
|
+
- Removed: `OpenapiFirst.app` and `OpenapiFirst.middleware`
|
6
|
+
- Removed: `OpenapiFirst::Coverage`
|
7
|
+
- Breaking: Parsed query and path parameters are available at `env[OpenapiFirst::PARAMS]`(or `env['openapi.params']`) instead of `OpenapiFirst::PARAMETERS`.
|
8
|
+
- Breaking: Request body and parameters now use string keys instead of symbols!
|
9
|
+
- Breaking: Query parameters are now parsed exactly like in the API description via the openapi_parameters gem. This means a couple of things:
|
10
|
+
- Query parameters now support `explode: true` (default) and `explode: false` for array and object parameters.
|
11
|
+
- Query parameters with brackets like 'filter[tag]' are no longer deconstructed into nested hashes, but accessible via the `filter[tag]` key.
|
12
|
+
- Query parameters are no longer interpreted as `style: deepObject` by default. If you want to use `style: deepObject`, for example to pass a nested hash as a query parameter like `filter[tag]`, you have to set `style: deepObject` explicitly.
|
13
|
+
- Path parameters are now parsed exactly as in the API description via the openapi_parameters gem.
|
4
14
|
|
5
15
|
## 0.21.0
|
6
16
|
|
data/Gemfile.lock
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
openapi_first (0.
|
4
|
+
openapi_first (1.0.0.beta1)
|
5
5
|
deep_merge (>= 1.2.1)
|
6
6
|
hanami-router (~> 2.0.0)
|
7
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)
|
11
|
-
|
11
|
+
mustermann-contrib (~> 3.0.0)
|
12
|
+
rack (>= 2.2, < 4.0)
|
12
13
|
|
13
14
|
GEM
|
14
15
|
remote: https://rubygems.org/
|
@@ -50,13 +51,16 @@ GEM
|
|
50
51
|
mustermann-contrib (3.0.0)
|
51
52
|
hansi (~> 0.2.0)
|
52
53
|
mustermann (= 3.0.0)
|
54
|
+
openapi_parameters (0.2.0)
|
55
|
+
rack (>= 2.2)
|
56
|
+
zeitwerk (~> 2.6)
|
53
57
|
parallel (1.22.1)
|
54
|
-
parser (3.2.1.
|
58
|
+
parser (3.2.1.1)
|
55
59
|
ast (~> 2.4.1)
|
56
60
|
pry (0.14.2)
|
57
61
|
coderay (~> 1.1)
|
58
62
|
method_source (~> 1.0)
|
59
|
-
rack (2.2.6.
|
63
|
+
rack (2.2.6.4)
|
60
64
|
rack-test (1.1.0)
|
61
65
|
rack (>= 1.0, < 3)
|
62
66
|
rainbow (3.1.1)
|
@@ -72,23 +76,23 @@ GEM
|
|
72
76
|
rspec-expectations (3.12.2)
|
73
77
|
diff-lcs (>= 1.2.0, < 2.0)
|
74
78
|
rspec-support (~> 3.12.0)
|
75
|
-
rspec-mocks (3.12.
|
79
|
+
rspec-mocks (3.12.5)
|
76
80
|
diff-lcs (>= 1.2.0, < 2.0)
|
77
81
|
rspec-support (~> 3.12.0)
|
78
82
|
rspec-support (3.12.0)
|
79
|
-
rubocop (1.
|
83
|
+
rubocop (1.48.1)
|
80
84
|
json (~> 2.3)
|
81
85
|
parallel (~> 1.10)
|
82
86
|
parser (>= 3.2.0.0)
|
83
87
|
rainbow (>= 2.2.2, < 4.0)
|
84
88
|
regexp_parser (>= 1.8, < 3.0)
|
85
89
|
rexml (>= 3.2.5, < 4.0)
|
86
|
-
rubocop-ast (>= 1.
|
90
|
+
rubocop-ast (>= 1.26.0, < 2.0)
|
87
91
|
ruby-progressbar (~> 1.7)
|
88
92
|
unicode-display_width (>= 2.4.0, < 3.0)
|
89
|
-
rubocop-ast (1.
|
93
|
+
rubocop-ast (1.28.0)
|
90
94
|
parser (>= 3.2.1.0)
|
91
|
-
ruby-progressbar (1.
|
95
|
+
ruby-progressbar (1.13.0)
|
92
96
|
ruby2_keywords (0.0.5)
|
93
97
|
unicode-display_width (2.4.2)
|
94
98
|
uri_template (0.7.0)
|
@@ -96,12 +100,11 @@ GEM
|
|
96
100
|
|
97
101
|
PLATFORMS
|
98
102
|
arm64-darwin-21
|
99
|
-
x86_64-darwin-20
|
100
|
-
x86_64-linux
|
101
103
|
|
102
104
|
DEPENDENCIES
|
103
105
|
bundler (~> 2)
|
104
106
|
openapi_first!
|
107
|
+
openapi_parameters (~> 0.2, <= 2.0.0)
|
105
108
|
pry
|
106
109
|
rack-test (~> 1)
|
107
110
|
rake (~> 13)
|
@@ -109,4 +112,4 @@ DEPENDENCIES
|
|
109
112
|
rubocop
|
110
113
|
|
111
114
|
BUNDLED WITH
|
112
|
-
2.3.
|
115
|
+
2.3.10
|
data/README.md
CHANGED
@@ -6,26 +6,11 @@ OpenapiFirst helps to implement HTTP APIs based on an [OpenApi](https://www.open
|
|
6
6
|
|
7
7
|
Start with writing an OpenAPI file that describes the API, which you are about to implement. Use a [validator](https://github.com/stoplightio/spectral/) to make sure the file is valid.
|
8
8
|
|
9
|
-
You can use OpenapiFirst via its [Rack middlewares](#rack-middlewares) or in [standalone mode](#standalone-usage).
|
10
|
-
|
11
|
-
## Alternatives
|
12
|
-
|
13
|
-
This gem is inspired by [committee](https://github.com/interagent/committee) (Ruby) and [connexion](https://github.com/zalando/connexion) (Python).
|
14
|
-
|
15
|
-
Here's a [comparison between committee and openapi_first](https://gist.github.com/ahx/1538c31f0652f459861713b5259e366a).
|
16
|
-
|
17
|
-
## Rack middlewares
|
18
|
-
|
19
9
|
OpenapiFirst consists of these Rack middlewares:
|
20
10
|
|
21
11
|
- [`OpenapiFirst::RequestValidation`](#OpenapiFirst::RequestValidation) – Validates the request against the API description and returns 400 if the request is invalid.
|
22
12
|
- [`OpenapiFirst::ResponseValidation`](#OpenapiFirst::ResponseValidation) Validates the response and raises an exception if the response body is invalid.
|
23
|
-
- [`OpenapiFirst::Router`](#OpenapiFirst::Router) – This internal middleware is added automatically
|
24
|
-
|
25
|
-
|
26
|
-
And these Rack apps:
|
27
|
-
- [`OpenapiFirst::Responder`](#OpenapiFirst::Responder) calls the [handler](#handlers) found for the operation, sets the correct content-type and serializes the response body to json if needed.
|
28
|
-
- [`OpenapiFirst::RackResponder`](#OpenapiFirst::RackResponder) calls the [handler](#handlers) found for the operation as a normal Rack application (`call(env)`) and returns the result as is.
|
13
|
+
- [`OpenapiFirst::Router`](#OpenapiFirst::Router) – This internal middleware is added automatically when using request/response validation. It adds the OpenAPI operation for the current request to the Rack env or returns 404 if no operation was found.
|
29
14
|
|
30
15
|
## OpenapiFirst::RequestValidation
|
31
16
|
|
@@ -35,6 +20,11 @@ This middleware returns a 400 status code with a body that describes the error i
|
|
35
20
|
use OpenapiFirst::RequestValidation, spec: 'openapi.yaml'
|
36
21
|
```
|
37
22
|
|
23
|
+
This will add these fields to the Rack env:
|
24
|
+
- `env[OpenapiFirst::OPERATION]` – The Operation object for the current request. This is an instance of `OpenapiFirst::Operation`.
|
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
|
+
|
38
28
|
### Options and defaults
|
39
29
|
|
40
30
|
| Name | Possible values | Description | Default |
|
@@ -62,26 +52,28 @@ content-type: "application/vnd.api+json"
|
|
62
52
|
}
|
63
53
|
```
|
64
54
|
|
65
|
-
|
66
|
-
|
67
|
-
### Parameter validation
|
55
|
+
### Parameters
|
68
56
|
|
69
|
-
The middleware filteres all top-level query parameters and paths parameters and tries to convert numeric values. Meaning, if you have an `:something_id` path with `type: integer`, it will try convert the value to an integer.
|
70
57
|
|
71
|
-
|
58
|
+
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:
|
72
59
|
|
73
|
-
|
60
|
+
```ruby
|
61
|
+
# GET /pets/filter[id]=1,2,3
|
62
|
+
env[OpenapiFirst::PARAMS] # => { 'filter[id]' => [1,2,3] }
|
74
63
|
|
75
|
-
|
64
|
+
# GET /colors/.blue.black.brown?format=csv
|
65
|
+
env[OpenapiFirst::PARAMS] # => { 'color_names' => ['blue', 'black', 'brown'], 'format' => 'csv' }
|
76
66
|
|
77
|
-
|
67
|
+
# And a lot more.
|
68
|
+
```
|
78
69
|
|
79
|
-
|
70
|
+
Integration for specific webframeworks is ongoing. Don't hesitate to create an issue with you specific needs.
|
80
71
|
|
81
72
|
### Request body validation
|
82
73
|
|
74
|
+
This middleware adds the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
|
75
|
+
|
83
76
|
The middleware will return a status `415` if the requests content type does not match or `400` if the request body is invalid.
|
84
|
-
This will also add the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
|
85
77
|
|
86
78
|
### Header, Cookie, Path parameter validation
|
87
79
|
|
@@ -116,7 +108,7 @@ This middleware is used automatically, but you can add it to the top of your mid
|
|
116
108
|
use OpenapiFirst::Router, spec: './openapi/openapi.yaml'
|
117
109
|
```
|
118
110
|
|
119
|
-
This middleware adds `env[OpenapiFirst::OPERATION]` which holds an Operation object that responds to `#operation_id`, `#path` (and `#[]` to access raw fields).
|
111
|
+
This middleware adds `env[OpenapiFirst::OPERATION]` which holds an Operation object that responds to `#operation_id`, `#path` (and `#[string]` to access raw fields).
|
120
112
|
|
121
113
|
### Options and defaults
|
122
114
|
|
@@ -126,115 +118,11 @@ This middleware adds `env[OpenapiFirst::OPERATION]` which holds an Operation obj
|
|
126
118
|
| `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) |
|
127
119
|
| `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) |
|
128
120
|
|
129
|
-
##
|
130
|
-
|
131
|
-
This Rack endpoint maps the HTTP request to a method call based on the [operationId](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operation-object) in your API description and calls it as a normal Rack application.
|
132
|
-
It does not not serialize objects as JSON or adds a content-type.
|
133
|
-
|
134
|
-
```ruby
|
135
|
-
run OpenapiFirst::RackResponder
|
136
|
-
```
|
137
|
-
|
138
|
-
### Options
|
139
|
-
|
140
|
-
| Name | Description |
|
141
|
-
| :----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
142
|
-
| `namespace:` | Optional. A class or module where to find the handler method. |
|
143
|
-
| `resolver:` | Optional. An object that responds to `#call(operation)` and returns a [handler](#handlers). By default this is an instance of [DefaultOperationResolver](#OpenapiFirst::DefaultOperationResolver) |
|
144
|
-
|
145
|
-
## OpenapiFirst::Responder
|
146
|
-
|
147
|
-
This Rack endpoint maps the HTTP request to a method call based on the [operationId](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operation-object) in your API description and calls it. Responder also adds a content-type to the response.
|
148
|
-
|
149
|
-
```ruby
|
150
|
-
run OpenapiFirst::Responder
|
151
|
-
```
|
152
|
-
|
153
|
-
### Options
|
154
|
-
|
155
|
-
| Name | Description |
|
156
|
-
| :----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
157
|
-
| `namespace:` | Optional. A class or module where to find the handler method. |
|
158
|
-
| `resolver:` | Optional. An object that responds to `#call(operation)` and returns a [handler](#handlers). By default this is an instance of [DefaultOperationResolver](#OpenapiFirst::DefaultOperationResolver) |
|
159
|
-
|
160
|
-
### OpenapiFirst::DefaultOperationResolver
|
161
|
-
|
162
|
-
This is the default way to look up a handler method for an operation. Handlers are always looked up in a namespace module that needs to be specified.
|
163
|
-
|
164
|
-
It works like this:
|
165
|
-
|
166
|
-
- An operationId "create_pet" or "createPet" or "create pet" calls `MyApi.create_pet(params, response)`
|
167
|
-
- "some_things.create" calls: `MyApi::SomeThings.create(params, response)`
|
168
|
-
- "pets#create" instantiates the class once (`MyApi::Pets::Create.new) and calls it on every request(`instance.call(params, response)`).
|
169
|
-
|
170
|
-
### Handlers
|
171
|
-
|
172
|
-
These handler methods are called with two arguments:
|
173
|
-
|
174
|
-
- `params` - Holds the parsed request body, filtered query params and path parameters (same as `env[OpenapiFirst::INBOX]`)
|
175
|
-
- `res` - Holds a Rack::Response that you can modify if needed
|
176
|
-
|
177
|
-
You can call `params.env` to access the Rack env (just like in [Hanami actions](https://guides.hanamirb.org/actions/parameters/))
|
178
|
-
|
179
|
-
There are two ways to set the response body:
|
180
|
-
|
181
|
-
- Calling `res.write "things"` (see [Rack::Response](https://www.rubydoc.info/github/rack/rack/Rack/Response))
|
182
|
-
- Returning a value which will get converted to JSON
|
183
|
-
|
184
|
-
## Standalone usage
|
185
|
-
|
186
|
-
Instead of composing these middlewares yourself you can use `OpenapiFirst.app`.
|
187
|
-
|
188
|
-
```ruby
|
189
|
-
module Pets
|
190
|
-
def self.find_pet(params, res)
|
191
|
-
{
|
192
|
-
id: params[:id],
|
193
|
-
name: 'Oscar'
|
194
|
-
}
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
# In config.ru:
|
199
|
-
require 'openapi_first'
|
200
|
-
run OpenapiFirst.app(
|
201
|
-
'./openapi/openapi.yaml',
|
202
|
-
namespace: Pets,
|
203
|
-
response_validation: ENV['RACK_ENV'] == 'test',
|
204
|
-
router_raise_error: ENV['RACK_ENV'] == 'test'
|
205
|
-
)
|
206
|
-
```
|
207
|
-
|
208
|
-
The above will use the mentioned Rack middlewares to:
|
209
|
-
|
210
|
-
- Validate the request and respond with 400 if the request does not match with your API description
|
211
|
-
- Map the request to a method call `Pets.find_pet` based on the `operationId` in the API description
|
212
|
-
- Set the response content type according to your spec (here with the default status code `200`)
|
213
|
-
|
214
|
-
### Options and defaults
|
215
|
-
|
216
|
-
| Name | Possible values | Description | Default |
|
217
|
-
| :-------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
218
|
-
| `spec_path` | | A filepath to an OpenAPI definition file. |
|
219
|
-
| `namespace:` | | A class or module where to find the handler methods. |
|
220
|
-
| `response_validation:` | `true`, `false` | If set to true it raises an exception if the response is invalid. This is useful during testing. | `false` |
|
221
|
-
| `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` |
|
222
|
-
| `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` |
|
223
|
-
| `resolver:` | | Option to customize finding the [handler](#handlers) method for an operation. See [OpenapiFirst::Responder](#OpenapiFirst::Responder) for details. |
|
224
|
-
|
225
|
-
Handler functions (`find_pet`) are called with two arguments:
|
226
|
-
|
227
|
-
- `params` - Holds the parsed request body, filtered query params and path parameters
|
228
|
-
- `res` - Holds a Rack::Response that you can modify if needed
|
229
|
-
If you want to access to plain Rack env you can call `params.env`.
|
230
|
-
|
231
|
-
## If your API description does not contain all endpoints
|
121
|
+
## Alternatives
|
232
122
|
|
233
|
-
|
234
|
-
run OpenapiFirst.middleware('./openapi/openapi.yaml', namespace: Pets)
|
235
|
-
```
|
123
|
+
This gem is inspired by [committee](https://github.com/interagent/committee) (Ruby) and [connexion](https://github.com/zalando/connexion) (Python).
|
236
124
|
|
237
|
-
Here
|
125
|
+
Here's a [comparison between committee and openapi_first](https://gist.github.com/ahx/1538c31f0652f459861713b5259e366a).
|
238
126
|
|
239
127
|
## Try it out
|
240
128
|
|
@@ -272,44 +160,6 @@ spec = OpenapiFirst.load('./openapi/openapi.yaml', only: { |path| path.starts_wi
|
|
272
160
|
run OpenapiFirst.app(spec, namespace: Pets)
|
273
161
|
```
|
274
162
|
|
275
|
-
## Coverage
|
276
|
-
|
277
|
-
(This is a bit experimental. Please try it out and give feedback.)
|
278
|
-
|
279
|
-
`OpenapiFirst::Coverage` helps you make sure, that you have called all endpoints of your OAS file when running tests via `rack-test`.
|
280
|
-
|
281
|
-
```ruby
|
282
|
-
# In your test (rspec example):
|
283
|
-
require 'openapi_first/coverage'
|
284
|
-
|
285
|
-
describe MyApp do
|
286
|
-
include Rack::Test::Methods
|
287
|
-
|
288
|
-
before(:all) do
|
289
|
-
@app_wrapper = OpenapiFirst::Coverage.new(MyApp, 'petstore.yaml')
|
290
|
-
end
|
291
|
-
|
292
|
-
after(:all) do
|
293
|
-
message = "The following paths have not been called yet: #{@app_wrapper.to_be_called}"
|
294
|
-
expect(@app_wrapper.to_be_called).to be_empty
|
295
|
-
end
|
296
|
-
|
297
|
-
# Overwrite `#app` to make rack-test call the wrapped app
|
298
|
-
def app
|
299
|
-
@app_wrapper
|
300
|
-
end
|
301
|
-
|
302
|
-
it 'does things' do
|
303
|
-
get '/i/my/stuff'
|
304
|
-
# …
|
305
|
-
end
|
306
|
-
end
|
307
|
-
```
|
308
|
-
|
309
|
-
## Mocking
|
310
|
-
|
311
|
-
Out of scope. Use [Prism](https://github.com/stoplightio/prism) or [fakeit](https://github.com/JustinFeng/fakeit).
|
312
|
-
|
313
163
|
## Development
|
314
164
|
|
315
165
|
Run `bin/setup` to install dependencies.
|
data/benchmarks/Gemfile.lock
CHANGED
@@ -1,24 +1,25 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ..
|
3
3
|
specs:
|
4
|
-
openapi_first (0.
|
4
|
+
openapi_first (1.0.0.beta1)
|
5
5
|
deep_merge (>= 1.2.1)
|
6
6
|
hanami-router (~> 2.0.0)
|
7
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)
|
11
|
-
|
11
|
+
mustermann-contrib (~> 3.0.0)
|
12
|
+
rack (>= 2.2, < 4.0)
|
12
13
|
|
13
14
|
GEM
|
14
15
|
remote: https://rubygems.org/
|
15
16
|
specs:
|
16
|
-
activesupport (7.0.4.
|
17
|
+
activesupport (7.0.4.3)
|
17
18
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
18
19
|
i18n (>= 1.6, < 2)
|
19
20
|
minitest (>= 5.1)
|
20
21
|
tzinfo (~> 2.0)
|
21
|
-
benchmark-ips (2.
|
22
|
+
benchmark-ips (2.12.0)
|
22
23
|
benchmark-memory (0.2.0)
|
23
24
|
memory_profiler (~> 1)
|
24
25
|
builder (3.2.4)
|
@@ -26,7 +27,7 @@ GEM
|
|
26
27
|
json_schema (~> 0.14, >= 0.14.3)
|
27
28
|
openapi_parser (~> 1.0)
|
28
29
|
rack (>= 1.5)
|
29
|
-
concurrent-ruby (1.2.
|
30
|
+
concurrent-ruby (1.2.2)
|
30
31
|
deep_merge (1.2.2)
|
31
32
|
dry-core (1.0.0)
|
32
33
|
concurrent-ruby (~> 1.0)
|
@@ -38,11 +39,11 @@ GEM
|
|
38
39
|
zeitwerk (~> 2.6)
|
39
40
|
dry-transformer (1.0.1)
|
40
41
|
zeitwerk (~> 2.6)
|
41
|
-
dry-types (1.7.
|
42
|
+
dry-types (1.7.1)
|
42
43
|
concurrent-ruby (~> 1.0)
|
43
|
-
dry-core (~> 1.0
|
44
|
-
dry-inflector (~> 1.0
|
45
|
-
dry-logic (
|
44
|
+
dry-core (~> 1.0)
|
45
|
+
dry-inflector (~> 1.0)
|
46
|
+
dry-logic (~> 1.4)
|
46
47
|
zeitwerk (~> 2.6)
|
47
48
|
ecma-re-validator (0.4.0)
|
48
49
|
regexp_parser (~> 2.2)
|
@@ -76,7 +77,7 @@ GEM
|
|
76
77
|
regexp_parser (~> 2.0)
|
77
78
|
uri_template (~> 0.7)
|
78
79
|
memory_profiler (1.0.1)
|
79
|
-
minitest (5.
|
80
|
+
minitest (5.18.0)
|
80
81
|
multi_json (1.15.0)
|
81
82
|
mustermann (3.0.0)
|
82
83
|
ruby2_keywords (~> 0.0.1)
|
@@ -87,15 +88,15 @@ GEM
|
|
87
88
|
mustermann (>= 1.0.0)
|
88
89
|
nio4r (2.5.8)
|
89
90
|
openapi_parser (1.0.0)
|
90
|
-
puma (6.
|
91
|
+
puma (6.2.0)
|
91
92
|
nio4r (~> 2.0)
|
92
|
-
rack (2.2.6.
|
93
|
+
rack (2.2.6.4)
|
93
94
|
rack-accept (0.4.5)
|
94
95
|
rack (>= 0.4)
|
95
96
|
rack-protection (3.0.5)
|
96
97
|
rack
|
97
|
-
regexp_parser (2.
|
98
|
-
roda (3.
|
98
|
+
regexp_parser (2.8.0)
|
99
|
+
roda (3.66.0)
|
99
100
|
rack
|
100
101
|
ruby2_keywords (0.0.5)
|
101
102
|
seg (1.2.0)
|
@@ -107,7 +108,7 @@ GEM
|
|
107
108
|
syro (3.2.1)
|
108
109
|
rack (>= 1.6.0)
|
109
110
|
seg
|
110
|
-
tilt (2.0
|
111
|
+
tilt (2.1.0)
|
111
112
|
tzinfo (2.0.6)
|
112
113
|
concurrent-ruby (~> 1.0)
|
113
114
|
uri_template (0.7.0)
|
data/examples/app.rb
CHANGED
@@ -1,22 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'openapi_first'
|
4
|
+
require 'rack'
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
6
|
+
# This example is a bit contrived, but it shows what you could do with the middlewares
|
7
|
+
|
8
|
+
App = Rack::Builder.new do
|
9
|
+
use OpenapiFirst::RequestValidation, raise_error: true, spec: File.expand_path('./openapi.yaml', __dir__)
|
10
|
+
use OpenapiFirst::ResponseValidation
|
14
11
|
|
15
|
-
|
12
|
+
handlers = {
|
13
|
+
'things#index' => ->(_env) { [200, { 'Content-Type' => 'application/json' }, ['{"hello": "world"}']] }
|
14
|
+
}
|
15
|
+
not_found = ->(_env) { [404, {}, []] }
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
namespace: Web,
|
20
|
-
router_raise_error: OpenapiFirst.env == 'test',
|
21
|
-
response_validation: OpenapiFirst.env == 'test'
|
22
|
-
)
|
17
|
+
run ->(env) { handlers.fetch(env[OpenapiFirst::OPERATION].operation_id, not_found).call(env) }
|
18
|
+
end
|
@@ -4,10 +4,9 @@ require 'forwardable'
|
|
4
4
|
require 'set'
|
5
5
|
require_relative 'schema_validation'
|
6
6
|
require_relative 'utils'
|
7
|
-
require_relative 'response_object'
|
8
7
|
|
9
8
|
module OpenapiFirst
|
10
|
-
class Operation
|
9
|
+
class Operation
|
11
10
|
extend Forwardable
|
12
11
|
def_delegators :operation_object,
|
13
12
|
:[],
|
@@ -40,24 +39,6 @@ module OpenapiFirst
|
|
40
39
|
operation_object['requestBody']
|
41
40
|
end
|
42
41
|
|
43
|
-
def parameters_schema
|
44
|
-
@parameters_schema ||= begin
|
45
|
-
parameters_json_schema = build_parameters_json_schema
|
46
|
-
parameters_json_schema && SchemaValidation.new(parameters_json_schema)
|
47
|
-
end
|
48
|
-
end
|
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
|
-
|
57
|
-
def content_types_for(status)
|
58
|
-
response_for(status)['content']&.keys
|
59
|
-
end
|
60
|
-
|
61
42
|
def response_schema_for(status, content_type)
|
62
43
|
content = response_for(status)['content']
|
63
44
|
return if content.nil? || content.empty?
|
@@ -102,6 +83,23 @@ module OpenapiFirst
|
|
102
83
|
!!find_content_for_content_type(content, request_content_type)
|
103
84
|
end
|
104
85
|
|
86
|
+
def query_parameters
|
87
|
+
@query_parameters ||= all_parameters.filter { |p| p['in'] == 'query' }
|
88
|
+
end
|
89
|
+
|
90
|
+
def path_parameters
|
91
|
+
@path_parameters ||= all_parameters.filter { |p| p['in'] == 'path' }
|
92
|
+
end
|
93
|
+
|
94
|
+
def all_parameters
|
95
|
+
@all_parameters ||= begin
|
96
|
+
parameters = @path_item_object['parameters']&.dup || []
|
97
|
+
parameters_on_operation = operation_object['parameters']
|
98
|
+
parameters.concat(parameters_on_operation) if parameters_on_operation
|
99
|
+
parameters
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
105
103
|
private
|
106
104
|
|
107
105
|
def response_by_code(status)
|
@@ -121,55 +119,5 @@ module OpenapiFirst
|
|
121
119
|
content[type] || content["#{type.split('/')[0]}/*"] || content['*/*']
|
122
120
|
end
|
123
121
|
end
|
124
|
-
|
125
|
-
def build_parameters_json_schema
|
126
|
-
parameters = all_parameters
|
127
|
-
return unless parameters&.any?
|
128
|
-
|
129
|
-
parameters.each_with_object(new_node) do |parameter, schema|
|
130
|
-
params = Rack::Utils.parse_nested_query(parameter['name'])
|
131
|
-
generate_schema(schema, params, parameter)
|
132
|
-
end
|
133
|
-
end
|
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
|
-
|
145
|
-
def all_parameters
|
146
|
-
parameters = @path_item_object['parameters']&.dup || []
|
147
|
-
parameters_on_operation = operation_object['parameters']
|
148
|
-
parameters.concat(parameters_on_operation) if parameters_on_operation
|
149
|
-
parameters
|
150
|
-
end
|
151
|
-
|
152
|
-
def generate_schema(schema, params, parameter)
|
153
|
-
required = Set.new(schema['required'])
|
154
|
-
params.each do |key, value|
|
155
|
-
required << key if parameter['required']
|
156
|
-
if value.is_a? Hash
|
157
|
-
property_schema = new_node
|
158
|
-
generate_schema(property_schema, value, parameter)
|
159
|
-
Utils.deep_merge!(schema['properties'], { key => property_schema })
|
160
|
-
else
|
161
|
-
schema['properties'][key] = parameter['schema']
|
162
|
-
end
|
163
|
-
end
|
164
|
-
schema['required'] = required.to_a
|
165
|
-
end
|
166
|
-
|
167
|
-
def new_node
|
168
|
-
{
|
169
|
-
'type' => 'object',
|
170
|
-
'required' => [],
|
171
|
-
'properties' => {}
|
172
|
-
}
|
173
|
-
end
|
174
122
|
end
|
175
123
|
end
|
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
require 'rack'
|
4
4
|
require 'multi_json'
|
5
|
-
require_relative 'inbox'
|
6
5
|
require_relative 'use_router'
|
7
6
|
require_relative 'validation_format'
|
7
|
+
require 'openapi_parameters'
|
8
8
|
|
9
9
|
module OpenapiFirst
|
10
|
-
class RequestValidation
|
10
|
+
class RequestValidation
|
11
11
|
prepend UseRouter
|
12
12
|
|
13
13
|
def initialize(app, options = {})
|
@@ -19,17 +19,17 @@ module OpenapiFirst
|
|
19
19
|
operation = env[OPERATION]
|
20
20
|
return @app.call(env) unless operation
|
21
21
|
|
22
|
-
env[INBOX] = {}
|
23
22
|
error = catch(:error) do
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
query_params = OpenapiParameters::Query.new(operation.query_parameters).unpack(env['QUERY_STRING'])
|
24
|
+
validate_query_parameters!(operation, query_params)
|
25
|
+
env[PARAMS].merge!(query_params)
|
26
|
+
|
27
27
|
return @app.call(env) unless operation.request_body
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
env[REQUEST_BODY]
|
32
|
-
|
29
|
+
content_type = Rack::Request.new(env).content_type
|
30
|
+
validate_request_content_type!(operation, content_type)
|
31
|
+
parsed_request_body = env[REQUEST_BODY]
|
32
|
+
validate_request_body!(operation, parsed_request_body, content_type)
|
33
33
|
nil
|
34
34
|
end
|
35
35
|
if error
|
@@ -42,23 +42,15 @@ module OpenapiFirst
|
|
42
42
|
|
43
43
|
private
|
44
44
|
|
45
|
-
|
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)
|
51
|
-
|
45
|
+
def validate_request_body!(operation, body, content_type)
|
52
46
|
validate_request_body_presence!(body, operation)
|
53
|
-
return if
|
47
|
+
return if content_type.nil?
|
54
48
|
|
55
|
-
schema = operation&.request_body_schema(
|
49
|
+
schema = operation&.request_body_schema(content_type)
|
56
50
|
return unless schema
|
57
51
|
|
58
52
|
errors = schema.validate(body)
|
59
53
|
throw_error(400, serialize_request_body_errors(errors)) if errors.any?
|
60
|
-
return Utils.deep_symbolize(body) if body.is_a?(Hash)
|
61
|
-
|
62
54
|
body
|
63
55
|
end
|
64
56
|
|
@@ -104,30 +96,29 @@ module OpenapiFirst
|
|
104
96
|
end
|
105
97
|
end
|
106
98
|
|
107
|
-
def
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
99
|
+
def build_json_schema(parameter_defs)
|
100
|
+
init_schema = {
|
101
|
+
'type' => 'object',
|
102
|
+
'properties' => {},
|
103
|
+
'required' => []
|
104
|
+
}
|
105
|
+
parameter_defs.each_with_object(init_schema) do |parameter_def, schema|
|
106
|
+
parameter = OpenapiParameters::Parameter.new(parameter_def)
|
107
|
+
schema['properties'][parameter.name] = parameter.schema if parameter.schema
|
108
|
+
schema['required'] << parameter.name if parameter.required?
|
109
|
+
end
|
116
110
|
end
|
117
111
|
|
118
|
-
def
|
119
|
-
|
120
|
-
|
121
|
-
parameter_name = key_value[0].to_sym
|
122
|
-
schema = key_value[1]
|
123
|
-
next unless params.key?(parameter_name)
|
112
|
+
def validate_query_parameters!(operation, params)
|
113
|
+
parameter_defs = operation.query_parameters
|
114
|
+
return unless parameter_defs&.any?
|
124
115
|
|
125
|
-
|
126
|
-
|
127
|
-
|
116
|
+
json_schema = build_json_schema(parameter_defs)
|
117
|
+
errors = SchemaValidation.new(json_schema).validate(params)
|
118
|
+
throw_error(400, serialize_parameter_errors(errors)) if errors.any?
|
128
119
|
end
|
129
120
|
|
130
|
-
def
|
121
|
+
def serialize_parameter_errors(validation_errors)
|
131
122
|
validation_errors.map do |error|
|
132
123
|
pointer = error['data_pointer'][1..].to_s
|
133
124
|
{
|
@@ -135,41 +126,5 @@ module OpenapiFirst
|
|
135
126
|
}.update(ValidationFormat.error_details(error))
|
136
127
|
end
|
137
128
|
end
|
138
|
-
|
139
|
-
def parse_parameter(value, schema)
|
140
|
-
return filtered_params(schema, value) if schema['properties']
|
141
|
-
|
142
|
-
return parse_array_parameter(value, schema) if schema['type'] == 'array'
|
143
|
-
|
144
|
-
parse_simple_value(value, schema)
|
145
|
-
end
|
146
|
-
|
147
|
-
def parse_array_parameter(value, schema)
|
148
|
-
return value if value.nil? || value.empty?
|
149
|
-
|
150
|
-
array = value.is_a?(Array) ? value : value.split(',')
|
151
|
-
return array unless schema['items']
|
152
|
-
|
153
|
-
array.map! { |e| parse_simple_value(e, schema['items']) }
|
154
|
-
end
|
155
|
-
|
156
|
-
def parse_simple_value(value, schema)
|
157
|
-
return to_boolean(value) if schema['type'] == 'boolean'
|
158
|
-
|
159
|
-
begin
|
160
|
-
return Integer(value, 10) if schema['type'] == 'integer'
|
161
|
-
return Float(value) if schema['type'] == 'number'
|
162
|
-
rescue ArgumentError
|
163
|
-
value
|
164
|
-
end
|
165
|
-
value
|
166
|
-
end
|
167
|
-
|
168
|
-
def to_boolean(value)
|
169
|
-
return true if value == 'true'
|
170
|
-
return false if value == 'false'
|
171
|
-
|
172
|
-
value
|
173
|
-
end
|
174
129
|
end
|
175
130
|
end
|
data/lib/openapi_first/router.rb
CHANGED
@@ -12,7 +12,6 @@ module OpenapiFirst
|
|
12
12
|
options
|
13
13
|
)
|
14
14
|
@app = app
|
15
|
-
@parent_app = options.fetch(:parent_app, nil)
|
16
15
|
@raise = options.fetch(:raise_error, false)
|
17
16
|
@not_found = options.fetch(:not_found, :halt)
|
18
17
|
spec = options.fetch(:spec)
|
@@ -28,8 +27,6 @@ module OpenapiFirst
|
|
28
27
|
env[OPERATION] = nil
|
29
28
|
response = call_router(env)
|
30
29
|
if env[OPERATION].nil?
|
31
|
-
return @parent_app.call(env) if @parent_app # This should only happen if used via OpenapiFirst.middleware
|
32
|
-
|
33
30
|
raise_error(env) if @raise
|
34
31
|
|
35
32
|
return @app.call(env) if @not_found == :continue
|
@@ -39,6 +36,10 @@ module OpenapiFirst
|
|
39
36
|
end
|
40
37
|
|
41
38
|
ORIGINAL_PATH = 'openapi_first.path_info'
|
39
|
+
private_constant :ORIGINAL_PATH
|
40
|
+
|
41
|
+
ROUTER_PARSED_BODY = 'router.parsed_body'
|
42
|
+
private_constant :ROUTER_PARSED_BODY
|
42
43
|
|
43
44
|
private
|
44
45
|
|
@@ -96,8 +97,11 @@ module OpenapiFirst
|
|
96
97
|
def build_route(operation)
|
97
98
|
lambda do |env|
|
98
99
|
env[OPERATION] = operation
|
99
|
-
|
100
|
-
env[
|
100
|
+
path_info = env.delete(ORIGINAL_PATH)
|
101
|
+
env[REQUEST_BODY] = env.delete(ROUTER_PARSED_BODY) if env.key?(ROUTER_PARSED_BODY)
|
102
|
+
route_params = Utils::StringKeyedHash.new(env['router.params'])
|
103
|
+
env[PARAMS] = OpenapiParameters::Path.new(operation.path_parameters).unpack(route_params)
|
104
|
+
env[Rack::PATH_INFO] = path_info
|
101
105
|
@app.call(env)
|
102
106
|
end
|
103
107
|
end
|
data/lib/openapi_first/utils.rb
CHANGED
@@ -18,12 +18,18 @@ module OpenapiFirst
|
|
18
18
|
Hanami::Utils::String.classify(string)
|
19
19
|
end
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
class StringKeyedHash
|
22
|
+
def initialize(original)
|
23
|
+
@orig = original
|
24
|
+
end
|
25
|
+
|
26
|
+
def key?(key)
|
27
|
+
@orig.key?(key.to_sym)
|
28
|
+
end
|
24
29
|
|
25
|
-
|
26
|
-
|
30
|
+
def [](key)
|
31
|
+
@orig[key.to_sym]
|
32
|
+
end
|
27
33
|
end
|
28
34
|
end
|
29
35
|
end
|
data/lib/openapi_first.rb
CHANGED
@@ -5,19 +5,15 @@ require 'json_refs'
|
|
5
5
|
require_relative 'openapi_first/definition'
|
6
6
|
require_relative 'openapi_first/version'
|
7
7
|
require_relative 'openapi_first/errors'
|
8
|
-
require_relative 'openapi_first/inbox'
|
9
8
|
require_relative 'openapi_first/router'
|
10
9
|
require_relative 'openapi_first/request_validation'
|
11
10
|
require_relative 'openapi_first/response_validator'
|
12
11
|
require_relative 'openapi_first/response_validation'
|
13
|
-
require_relative 'openapi_first/responder'
|
14
|
-
require_relative 'openapi_first/app'
|
15
12
|
|
16
13
|
module OpenapiFirst
|
17
|
-
OPERATION = '
|
18
|
-
|
19
|
-
REQUEST_BODY = '
|
20
|
-
INBOX = 'openapi_first.inbox'
|
14
|
+
OPERATION = 'openapi.operation'
|
15
|
+
PARAMS = 'openapi.params'
|
16
|
+
REQUEST_BODY = 'openapi.parsed_request_body'
|
21
17
|
HANDLER = 'openapi_first.handler'
|
22
18
|
|
23
19
|
def self.env
|
@@ -50,32 +46,4 @@ module OpenapiFirst
|
|
50
46
|
response_validation: response_validation
|
51
47
|
)
|
52
48
|
end
|
53
|
-
|
54
|
-
def self.middleware(
|
55
|
-
spec,
|
56
|
-
namespace:,
|
57
|
-
router_raise_error: false,
|
58
|
-
request_validation_raise_error: false,
|
59
|
-
response_validation: false
|
60
|
-
)
|
61
|
-
spec = OpenapiFirst.load(spec) unless spec.is_a?(Definition)
|
62
|
-
AppWithOptions.new(
|
63
|
-
spec,
|
64
|
-
namespace: namespace,
|
65
|
-
router_raise_error: router_raise_error,
|
66
|
-
request_validation_raise_error: request_validation_raise_error,
|
67
|
-
response_validation: response_validation
|
68
|
-
)
|
69
|
-
end
|
70
|
-
|
71
|
-
class AppWithOptions
|
72
|
-
def initialize(spec, options)
|
73
|
-
@spec = spec
|
74
|
-
@options = options
|
75
|
-
end
|
76
|
-
|
77
|
-
def new(app)
|
78
|
-
App.new(app, @spec, **@options)
|
79
|
-
end
|
80
|
-
end
|
81
49
|
end
|
data/openapi_first.gemspec
CHANGED
@@ -40,9 +40,11 @@ Gem::Specification.new do |spec|
|
|
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'
|
43
|
-
spec.add_runtime_dependency '
|
43
|
+
spec.add_runtime_dependency 'mustermann-contrib', '~> 3.0.0'
|
44
|
+
spec.add_runtime_dependency 'rack', '>= 2.2', '< 4.0'
|
44
45
|
|
45
46
|
spec.add_development_dependency 'bundler', '~> 2'
|
47
|
+
spec.add_development_dependency 'openapi_parameters', '~> 0.2', '<= 2.0.0'
|
46
48
|
spec.add_development_dependency 'rack-test', '~> 1'
|
47
49
|
spec.add_development_dependency 'rake', '~> 13'
|
48
50
|
spec.add_development_dependency 'rspec', '~> 3'
|
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: 1.0.0.beta1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andreas Haller
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-04-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deep_merge
|
@@ -101,19 +101,39 @@ dependencies:
|
|
101
101
|
- !ruby/object:Gem::Version
|
102
102
|
version: '1.14'
|
103
103
|
- !ruby/object:Gem::Dependency
|
104
|
-
name:
|
104
|
+
name: mustermann-contrib
|
105
105
|
requirement: !ruby/object:Gem::Requirement
|
106
106
|
requirements:
|
107
107
|
- - "~>"
|
108
108
|
- !ruby/object:Gem::Version
|
109
|
-
version:
|
109
|
+
version: 3.0.0
|
110
110
|
type: :runtime
|
111
111
|
prerelease: false
|
112
112
|
version_requirements: !ruby/object:Gem::Requirement
|
113
113
|
requirements:
|
114
114
|
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: 3.0.0
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: rack
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
115
122
|
- !ruby/object:Gem::Version
|
116
123
|
version: '2.2'
|
124
|
+
- - "<"
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '4.0'
|
127
|
+
type: :runtime
|
128
|
+
prerelease: false
|
129
|
+
version_requirements: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '2.2'
|
134
|
+
- - "<"
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '4.0'
|
117
137
|
- !ruby/object:Gem::Dependency
|
118
138
|
name: bundler
|
119
139
|
requirement: !ruby/object:Gem::Requirement
|
@@ -128,6 +148,26 @@ dependencies:
|
|
128
148
|
- - "~>"
|
129
149
|
- !ruby/object:Gem::Version
|
130
150
|
version: '2'
|
151
|
+
- !ruby/object:Gem::Dependency
|
152
|
+
name: openapi_parameters
|
153
|
+
requirement: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - "~>"
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0.2'
|
158
|
+
- - "<="
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: 2.0.0
|
161
|
+
type: :development
|
162
|
+
prerelease: false
|
163
|
+
version_requirements: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - "~>"
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '0.2'
|
168
|
+
- - "<="
|
169
|
+
- !ruby/object:Gem::Version
|
170
|
+
version: 2.0.0
|
131
171
|
- !ruby/object:Gem::Dependency
|
132
172
|
name: rack-test
|
133
173
|
requirement: !ruby/object:Gem::Requirement
|
@@ -214,25 +254,17 @@ files:
|
|
214
254
|
- examples/config.ru
|
215
255
|
- examples/openapi.yaml
|
216
256
|
- lib/openapi_first.rb
|
217
|
-
- lib/openapi_first/app.rb
|
218
257
|
- lib/openapi_first/body_parser_middleware.rb
|
219
|
-
- lib/openapi_first/coverage.rb
|
220
|
-
- lib/openapi_first/default_operation_resolver.rb
|
221
258
|
- lib/openapi_first/definition.rb
|
222
259
|
- lib/openapi_first/errors.rb
|
223
|
-
- lib/openapi_first/inbox.rb
|
224
260
|
- lib/openapi_first/operation.rb
|
225
|
-
- lib/openapi_first/rack_responder.rb
|
226
261
|
- lib/openapi_first/request_validation.rb
|
227
|
-
- lib/openapi_first/responder.rb
|
228
|
-
- lib/openapi_first/response_object.rb
|
229
262
|
- lib/openapi_first/response_validation.rb
|
230
263
|
- lib/openapi_first/response_validator.rb
|
231
264
|
- lib/openapi_first/router.rb
|
232
265
|
- lib/openapi_first/schema_validation.rb
|
233
266
|
- lib/openapi_first/use_router.rb
|
234
267
|
- lib/openapi_first/utils.rb
|
235
|
-
- lib/openapi_first/validation.rb
|
236
268
|
- lib/openapi_first/validation_format.rb
|
237
269
|
- lib/openapi_first/version.rb
|
238
270
|
- openapi_first.gemspec
|
@@ -252,9 +284,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
252
284
|
version: 3.0.5
|
253
285
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
254
286
|
requirements:
|
255
|
-
- - "
|
287
|
+
- - ">"
|
256
288
|
- !ruby/object:Gem::Version
|
257
|
-
version:
|
289
|
+
version: 1.3.1
|
258
290
|
requirements: []
|
259
291
|
rubygems_version: 3.3.7
|
260
292
|
signing_key:
|
data/lib/openapi_first/app.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rack'
|
4
|
-
|
5
|
-
module OpenapiFirst
|
6
|
-
class App
|
7
|
-
def initialize( # rubocop:disable Metrics/ParameterLists
|
8
|
-
parent_app,
|
9
|
-
spec,
|
10
|
-
namespace:,
|
11
|
-
router_raise_error: false,
|
12
|
-
request_validation_raise_error: false,
|
13
|
-
response_validation: false,
|
14
|
-
resolver: nil
|
15
|
-
)
|
16
|
-
@stack = Rack::Builder.app do
|
17
|
-
freeze_app
|
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
|
21
|
-
run OpenapiFirst::Responder.new(namespace: namespace, resolver: resolver)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def call(env)
|
26
|
-
@stack.call(env)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
@@ -1,28 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module OpenapiFirst
|
4
|
-
class Coverage
|
5
|
-
attr_reader :to_be_called
|
6
|
-
|
7
|
-
def initialize(app, spec)
|
8
|
-
@app = app
|
9
|
-
@spec = spec
|
10
|
-
@to_be_called = spec.operations.map do |operation|
|
11
|
-
endpoint_id(operation)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def call(env)
|
16
|
-
response = @app.call(env)
|
17
|
-
operation = env[OPERATION]
|
18
|
-
@to_be_called.delete(endpoint_id(operation)) if operation
|
19
|
-
response
|
20
|
-
end
|
21
|
-
|
22
|
-
private
|
23
|
-
|
24
|
-
def endpoint_id(operation)
|
25
|
-
"#{operation.path}##{operation.method}"
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
@@ -1,63 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'utils'
|
4
|
-
|
5
|
-
module OpenapiFirst
|
6
|
-
class DefaultOperationResolver
|
7
|
-
def initialize(namespace)
|
8
|
-
@namespace = namespace
|
9
|
-
@handlers = {}
|
10
|
-
end
|
11
|
-
|
12
|
-
def call(operation)
|
13
|
-
@handlers[operation.name] ||= begin
|
14
|
-
id = handler_id(operation)
|
15
|
-
find_handler(id) if id
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
def find_handler(id)
|
20
|
-
name = id.match(/:*(.*)/)&.to_a&.at(1)
|
21
|
-
return if name.nil?
|
22
|
-
|
23
|
-
catch :halt do
|
24
|
-
return find_class_method_handler(name) if name.include?('.')
|
25
|
-
return find_instance_method_handler(name) if name.include?('#')
|
26
|
-
end
|
27
|
-
method_name = Utils.underscore(name)
|
28
|
-
return unless @namespace.respond_to?(method_name)
|
29
|
-
|
30
|
-
@namespace.method(method_name)
|
31
|
-
end
|
32
|
-
|
33
|
-
def handler_id(operation)
|
34
|
-
id = operation['x-handler'] || operation['operationId']
|
35
|
-
if id.nil?
|
36
|
-
raise HandlerNotFoundError,
|
37
|
-
"operationId or x-handler is missing in '#{operation.method} #{operation.path}' so I cannot find a handler for this operation." # rubocop:disable Layout/LineLength
|
38
|
-
end
|
39
|
-
|
40
|
-
id
|
41
|
-
end
|
42
|
-
|
43
|
-
def find_class_method_handler(name)
|
44
|
-
module_name, method_name = name.split('.')
|
45
|
-
klass = find_const(@namespace, module_name)
|
46
|
-
klass.method(Utils.underscore(method_name))
|
47
|
-
end
|
48
|
-
|
49
|
-
def find_instance_method_handler(name)
|
50
|
-
module_name, klass_name = name.split('#')
|
51
|
-
const = find_const(@namespace, module_name)
|
52
|
-
klass = find_const(const, klass_name)
|
53
|
-
klass.new
|
54
|
-
end
|
55
|
-
|
56
|
-
def find_const(parent, name)
|
57
|
-
name = Utils.classify(name)
|
58
|
-
throw :halt unless parent.const_defined?(name, false)
|
59
|
-
|
60
|
-
parent.const_get(name, false)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
data/lib/openapi_first/inbox.rb
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rack'
|
4
|
-
require 'multi_json'
|
5
|
-
require_relative 'inbox'
|
6
|
-
require_relative 'default_operation_resolver'
|
7
|
-
|
8
|
-
module OpenapiFirst
|
9
|
-
class Responder
|
10
|
-
def initialize(namespace: nil, resolver: nil)
|
11
|
-
@resolver = resolver || DefaultOperationResolver.new(namespace)
|
12
|
-
@namespace = namespace
|
13
|
-
end
|
14
|
-
|
15
|
-
def call(env)
|
16
|
-
operation = env[OpenapiFirst::OPERATION]
|
17
|
-
res = Rack::Response.new
|
18
|
-
handler = find_handler(operation)
|
19
|
-
result = handler.call(inbox(env), res)
|
20
|
-
res.write serialize(result) if result && res.body.empty?
|
21
|
-
res[Rack::CONTENT_TYPE] ||= operation.content_types_for(res.status)&.first
|
22
|
-
res.finish
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
|
27
|
-
def inbox(env)
|
28
|
-
Inbox.new(env).tap { |i| i.merge!(env[INBOX]) if env[INBOX] }
|
29
|
-
end
|
30
|
-
|
31
|
-
def find_handler(operation)
|
32
|
-
handler = @resolver.call(operation)
|
33
|
-
raise NotImplementedError, "Could not find handler for #{operation.name}" unless handler
|
34
|
-
|
35
|
-
handler
|
36
|
-
end
|
37
|
-
|
38
|
-
def serialize(result)
|
39
|
-
return result if result.is_a?(String)
|
40
|
-
|
41
|
-
MultiJson.dump(result)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'forwardable'
|
4
|
-
|
5
|
-
module OpenapiFirst
|
6
|
-
# Represents an OpenAPI Response Object
|
7
|
-
class ResponseObject
|
8
|
-
extend Forwardable
|
9
|
-
def_delegators :@parsed,
|
10
|
-
:content
|
11
|
-
|
12
|
-
def_delegators :@raw,
|
13
|
-
:[]
|
14
|
-
|
15
|
-
def initialize(parsed)
|
16
|
-
@parsed = parsed
|
17
|
-
@raw = parsed.raw
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|