openapi_first 0.21.0 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b8b03aaa251de1bdb5cbba71de089bf1ffc6b9dbb96512008f88e783c3cea27
4
- data.tar.gz: 02eb6cec864e9b5ed272d4392b31fd9e006713d3a8b00df9b26e9a52fa68e555
3
+ metadata.gz: 2248133fb2e0b761fb314629dcbfa7c9a6b7e0ac03b59887fe158968b17ed827
4
+ data.tar.gz: b0012d8af3c9dee1fa94334be7890e260f0173946129b53ec8f930f768f8de28
5
5
  SHA512:
6
- metadata.gz: e37e99e982f0ead9d54587683fa74491e69998f520b1cfb4993c9e1ee81273537dfdf1751c77daef856ab3c3051660589d2f8ac6e58508fd11d3ea130734d2a5
7
- data.tar.gz: 6944ae2444da29928eeb12e70326505aa154f1c36a09f033083098b1e34be766b075257fd2459b94574370fa8cc417ef488f74dafcec6026eaab07a47f471445
6
+ metadata.gz: c615c847efcd10cfd145bcc99ede55da2eaa76fe91b9d0ab3fdb159c20b6cc314309f583ba0b784c3e3493a7a534ed1836309807c549bd8c40b226f2f9bf77e9
7
+ data.tar.gz: 83c476b9b67ce15ad71e052af8360e013659da13fbbd8f86a55bae9d4bbcf08f4ac291187dfccc5786e957c65d2a3124366f007998c675b4a3e29bccaecf332a
data/CHANGELOG.md CHANGED
@@ -1,6 +1,16 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
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.21.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
- rack (~> 2.2)
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.0)
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.2)
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.3)
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.45.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.24.1, < 2.0)
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.26.0)
93
+ rubocop-ast (1.28.0)
90
94
  parser (>= 3.2.1.0)
91
- ruby-progressbar (1.11.0)
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.7
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 before request/response validation. Finds the OpenAPI operation for the current request or returns 404 if no operation was found. This can be customized by adding it yourself.
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
- This middleware adds `env[OpenapiFirst::INBOX]` which holds the (filtered) path and query parameters and the parsed request body.
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
- It just works with a parameter with `name: filter[age]`.
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
- OpenapiFirst also supports `type: array` for query parameters and will convert `items` just as described above. [`style`](http://spec.openapis.org/oas/v3.0.3#style-values) and `explode` attributes are not supported for query parameters. It will always act as if `style: form` and `explode: false` were used for query parameters.
60
+ ```ruby
61
+ # GET /pets/filter[id]=1,2,3
62
+ env[OpenapiFirst::PARAMS] # => { 'filter[id]' => [1,2,3] }
74
63
 
75
- Conversion is currently done only for path and query parameters, but not for the request body. OpenapiFirst currently does not convert date, date-time or time formats.
64
+ # GET /colors/.blue.black.brown?format=csv
65
+ env[OpenapiFirst::PARAMS] # => { 'color_names' => ['blue', 'black', 'brown'], 'format' => 'csv' }
76
66
 
77
- If you want to forbid _nested_ query parameters you will need to use [`additionalProperties: false`](https://json-schema.org/understanding-json-schema/reference/object.html#properties) in your query parameter JSON schema.
67
+ # And a lot more.
68
+ ```
78
69
 
79
- _OpenapiFirst always treats query parameters like [`style: deepObject`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#style-values), **but** it just works with nested objects (`filter[foo][bar]=baz`) (see [this discussion](https://github.com/OAI/OpenAPI-Specification/issues/1706))._
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
- ## OpenapiFirst::RackResponder
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
- ```ruby
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 all requests that are not part of the API description will be passed to the next app.
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.
@@ -1,24 +1,25 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- openapi_first (0.21.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
- rack (~> 2.2)
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.2)
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.11.0)
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.0)
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.0)
42
+ dry-types (1.7.1)
42
43
  concurrent-ruby (~> 1.0)
43
- dry-core (~> 1.0, < 2)
44
- dry-inflector (~> 1.0, < 2)
45
- dry-logic (>= 1.4, < 2)
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.17.0)
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.1.0)
91
+ puma (6.2.0)
91
92
  nio4r (~> 2.0)
92
- rack (2.2.6.2)
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.7.0)
98
- roda (3.65.0)
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.11)
111
+ tilt (2.1.0)
111
112
  tzinfo (2.0.6)
112
113
  concurrent-ruby (~> 1.0)
113
114
  uri_template (0.7.0)
@@ -5,7 +5,7 @@ require 'openapi_first'
5
5
 
6
6
  namespace = Module.new do
7
7
  def self.find_thing(params, _res)
8
- { hello: 'world', id: params.fetch(:id) }
8
+ { hello: 'world', id: params.fetch('id') }
9
9
  end
10
10
 
11
11
  def self.find_things(_params, _res)
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
- module Web
6
- module Things
7
- class Index
8
- def call(_params, _response)
9
- { hello: 'world' }
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
- oas_path = File.absolute_path('./openapi.yaml', __dir__)
12
+ handlers = {
13
+ 'things#index' => ->(_env) { [200, { 'Content-Type' => 'application/json' }, ['{"hello": "world"}']] }
14
+ }
15
+ not_found = ->(_env) { [404, {}, []] }
16
16
 
17
- App = OpenapiFirst.app(
18
- oas_path,
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 # rubocop:disable Metrics/ClassLength
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 # rubocop:disable Metrics/ClassLength
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
- params = validate_query_parameters!(operation, env[PARAMETERS])
25
- env[INBOX].merge! env[PARAMETERS] = params if params
26
- req = Rack::Request.new(env)
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
- 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)
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
- 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)
51
-
45
+ def validate_request_body!(operation, body, content_type)
52
46
  validate_request_body_presence!(body, operation)
53
- return if body.nil?
47
+ return if content_type.nil?
54
48
 
55
- schema = operation&.request_body_schema(request.content_type)
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 validate_query_parameters!(operation, params)
108
- schema = operation.query_parameters_schema
109
- return unless schema
110
-
111
- params = filtered_params(schema.raw_schema, params)
112
- params = Utils.deep_stringify(params)
113
- errors = schema.validate(params)
114
- throw_error(400, serialize_query_parameter_errors(errors)) if errors.any?
115
- Utils.deep_symbolize(params)
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 filtered_params(json_schema, params)
119
- json_schema['properties']
120
- .each_with_object({}) do |key_value, result|
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
- value = params[parameter_name]
126
- result[parameter_name] = parse_parameter(value, schema)
127
- end
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 serialize_query_parameter_errors(validation_errors)
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
@@ -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
- env[PARAMETERS] = env['router.params']
100
- env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH)
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
@@ -18,12 +18,18 @@ module OpenapiFirst
18
18
  Hanami::Utils::String.classify(string)
19
19
  end
20
20
 
21
- def self.deep_symbolize(hash)
22
- Hanami::Utils::Hash.deep_symbolize(hash)
23
- end
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
- def self.deep_stringify(hash)
26
- Hanami::Utils::Hash.deep_stringify(hash)
30
+ def [](key)
31
+ @orig[key.to_sym]
32
+ end
27
33
  end
28
34
  end
29
35
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.21.0'
4
+ VERSION = '1.0.0.beta1'
5
5
  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 = 'openapi_first.operation'
18
- PARAMETERS = 'openapi_first.parameters'
19
- REQUEST_BODY = 'openapi_first.parsed_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
@@ -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 'rack', '~> 2.2'
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.21.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-03-06 00:00:00.000000000 Z
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: rack
104
+ name: mustermann-contrib
105
105
  requirement: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: '2.2'
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: '0'
289
+ version: 1.3.1
258
290
  requirements: []
259
291
  rubygems_version: 3.3.7
260
292
  signing_key:
@@ -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
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OpenapiFirst
4
- # An instance of this gets passed to handler functions in the Responder.
5
- class Inbox < Hash
6
- attr_reader :env
7
-
8
- def initialize(env)
9
- @env = env
10
- super()
11
- end
12
- end
13
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'responder'
4
-
5
- module OpenapiFirst
6
- class RackResponder < Responder
7
- def call(env)
8
- operation = env[OpenapiFirst::OPERATION]
9
- find_handler(operation)&.call(env)
10
- end
11
- end
12
- end
@@ -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
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OpenapiFirst
4
- class Validation
5
- attr_reader :errors
6
-
7
- def initialize(errors)
8
- @errors = errors
9
- end
10
-
11
- def errors?
12
- !errors.empty?
13
- end
14
- end
15
- end