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 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