openapi_first 0.17.0 → 0.20.0

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: a6e1915ef20b0c31bd58a5f0bbba3d91b9be2915c9c0e1598e085d4bea142a06
4
- data.tar.gz: 42c62a18aab203b1920adf1367cd9bc500c6e31b1e82fb8b4370ca34cedb372d
3
+ metadata.gz: 033e7d24a78ecd8cd0c98bad90e49949612c47a2d0b83791cca3ec7149e91ff6
4
+ data.tar.gz: f2092ebe5ee49b86b9745c2a1d27d4a71b798ed1d91f1fad6ea8cc9750f3ddff
5
5
  SHA512:
6
- metadata.gz: a7985ff17cce925cb8ed24b47efaea39f4a2bef3a7f029de3edbef38a6ae555e57784d1346cfa0acbfd18b3d70124c4019dd0a5533990dccfcc408667b70b693
7
- data.tar.gz: 8e28e55faf6de5246ff87c0943f507c2f93afe4d96f5bbde423feb15607295e7888d25c4add8054ec90103a4305161366f72c8088d0d8f64e053f9ba6902b592
6
+ metadata.gz: fc80eff5d2f0c30d1df07d9e7d58cec4dfab41b05467e503dfc4200c11670fe336d1f7ac723b770798708006419ddc1da689ce862b12385d673f03f9a4d4aef4
7
+ data.tar.gz: dbf86b5ce8a3b23f12af0311a5bad788b0b9841deff4f8170fe7de17acbad230b9a65806abf7eb73958bd123a76820f54b84f92a2c09a2dd54364c47bf5203f9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.20.0
6
+ - You can pass a filepath to `spec:` now so you no longer have to call `OpenapiFirst.load` anymore.
7
+ - Router is optional now.
8
+ You no longer have to add `Router` to your middleware stack. You still can add it to customize behaviour by setting options, but you no longer have to add it.
9
+ If you don't add the Router, make sure you pass `spec:` to your request/response validation middleware.
10
+ - Support "4xx" and "4XX" response definitions.
11
+ (4XX is defined in the standard, but 2xx is used in the wild as well 🦁.)
12
+ - Removed warning about missing operationId, because operationId is not used until the Responder is used.
13
+ - Raise HandlerNotFoundError when handler cannot be found
14
+
15
+ ## 0.19.0
16
+
17
+ - Add `RackResponder`
18
+
19
+ - BREAKING CHANGE: Handler classes are now instantiated only once without any arguments and the same instance is called on each following call/request.
20
+
21
+ ## 0.18.0
22
+
23
+ Yanked. No useful changes.
24
+
3
25
  ## 0.17.0
4
26
 
5
27
  - BREAKING CHANGE: Use a Hash instead of named arguments for middleware options for better compatibility
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (0.17.0)
4
+ openapi_first (0.20.0)
5
5
  deep_merge (>= 1.2.1)
6
6
  hanami-router (= 2.0.alpha5)
7
7
  hanami-utils (= 2.0.alpha3)
@@ -29,66 +29,69 @@ GEM
29
29
  hanami-utils (2.0.0.alpha3)
30
30
  concurrent-ruby (~> 1.0)
31
31
  dry-transformer (~> 0.1)
32
- hansi (0.2.0)
32
+ hansi (0.2.1)
33
+ json (2.6.2)
33
34
  json_refs (0.1.7)
34
35
  hana
35
- json_schemer (0.2.20)
36
+ json_schemer (0.2.22)
36
37
  ecma-re-validator (~> 0.3)
37
38
  hana (~> 1.3)
38
39
  regexp_parser (~> 2.0)
39
40
  uri_template (~> 0.7)
40
41
  method_source (1.0.0)
41
42
  multi_json (1.15.0)
42
- mustermann (1.1.1)
43
+ mustermann (1.1.2)
43
44
  ruby2_keywords (~> 0.0.1)
44
- mustermann-contrib (1.1.1)
45
+ mustermann-contrib (1.1.2)
45
46
  hansi (~> 0.2.0)
46
- mustermann (= 1.1.1)
47
+ mustermann (= 1.1.2)
47
48
  parallel (1.22.1)
48
- parser (3.1.1.0)
49
+ parser (3.1.2.1)
49
50
  ast (~> 2.4.1)
50
51
  pry (0.14.1)
51
52
  coderay (~> 1.1)
52
53
  method_source (~> 1.0)
53
- rack (2.2.3)
54
+ rack (2.2.4)
54
55
  rack-test (1.1.0)
55
56
  rack (>= 1.0, < 3)
56
57
  rainbow (3.1.1)
57
58
  rake (13.0.6)
58
- regexp_parser (2.2.1)
59
+ regexp_parser (2.6.0)
59
60
  rexml (3.2.5)
60
- rspec (3.11.0)
61
- rspec-core (~> 3.11.0)
62
- rspec-expectations (~> 3.11.0)
63
- rspec-mocks (~> 3.11.0)
64
- rspec-core (3.11.0)
65
- rspec-support (~> 3.11.0)
66
- rspec-expectations (3.11.0)
61
+ rspec (3.12.0)
62
+ rspec-core (~> 3.12.0)
63
+ rspec-expectations (~> 3.12.0)
64
+ rspec-mocks (~> 3.12.0)
65
+ rspec-core (3.12.0)
66
+ rspec-support (~> 3.12.0)
67
+ rspec-expectations (3.12.0)
67
68
  diff-lcs (>= 1.2.0, < 2.0)
68
- rspec-support (~> 3.11.0)
69
- rspec-mocks (3.11.1)
69
+ rspec-support (~> 3.12.0)
70
+ rspec-mocks (3.12.0)
70
71
  diff-lcs (>= 1.2.0, < 2.0)
71
- rspec-support (~> 3.11.0)
72
- rspec-support (3.11.0)
73
- rubocop (1.27.0)
72
+ rspec-support (~> 3.12.0)
73
+ rspec-support (3.12.0)
74
+ rubocop (1.37.1)
75
+ json (~> 2.3)
74
76
  parallel (~> 1.10)
75
- parser (>= 3.1.0.0)
77
+ parser (>= 3.1.2.1)
76
78
  rainbow (>= 2.2.2, < 4.0)
77
79
  regexp_parser (>= 1.8, < 3.0)
78
- rexml
79
- rubocop-ast (>= 1.16.0, < 2.0)
80
+ rexml (>= 3.2.5, < 4.0)
81
+ rubocop-ast (>= 1.23.0, < 2.0)
80
82
  ruby-progressbar (~> 1.7)
81
83
  unicode-display_width (>= 1.4.0, < 3.0)
82
- rubocop-ast (1.16.0)
84
+ rubocop-ast (1.23.0)
83
85
  parser (>= 3.1.1.0)
84
86
  ruby-progressbar (1.11.0)
85
87
  ruby2_keywords (0.0.5)
86
- unicode-display_width (2.1.0)
88
+ unicode-display_width (2.3.0)
87
89
  uri_template (0.7.0)
88
90
 
89
91
  PLATFORMS
90
92
  arm64-darwin-21
91
93
  x86_64-darwin-20
94
+ x86_64-linux
92
95
 
93
96
  DEPENDENCIES
94
97
  bundler (~> 2)
data/README.md CHANGED
@@ -18,41 +18,28 @@ Here's a [comparison between committee and openapi_first](https://gist.github.co
18
18
 
19
19
  OpenapiFirst consists of these Rack middlewares:
20
20
 
21
- - [`OpenapiFirst::Router`](#OpenapiFirst::Router) – Finds the OpenAPI operation for the current request or returns 404 if no operation was found. This can be customized.
22
21
  - [`OpenapiFirst::RequestValidation`](#OpenapiFirst::RequestValidation) – Validates the request against the API description and returns 400 if the request is invalid.
23
- - [`OpenapiFirst::Responder`](#OpenapiFirst::Responder) calls the [handler](#handlers) found for the operation, sets the correct content-type and serialized the response body to json if needed.
24
22
  - [`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.
25
24
 
26
- ## OpenapiFirst::Router
27
-
28
- You always have to add this middleware first in order to make the other middlewares work.
29
-
30
- ```ruby
31
- use OpenapiFirst::Router, spec: OpenapiFirst.load('./openapi/openapi.yaml')
32
- ```
33
-
34
- This middleware adds `env[OpenapiFirst::OPERATION]` which holds an Operation object that responds to `#operation_id`, `#path` (and `#[]` to access raw fields).
35
25
 
36
- ### Options and defaults
37
-
38
- | Name | Possible values | Description | Default |
39
- | :------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
40
- | `spec:` | | The spec loaded via `OpenapiFirst.load` | |
41
- | `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) |
42
- | `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) |
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.
43
29
 
44
30
  ## OpenapiFirst::RequestValidation
45
31
 
46
32
  This middleware returns a 400 status code with a body that describes the error if the request is not valid.
47
33
 
48
34
  ```ruby
49
- use OpenapiFirst::RequestValidation
35
+ use OpenapiFirst::RequestValidatio, spec: 'openapi.yaml'
50
36
  ```
51
37
 
52
38
  ### Options and defaults
53
39
 
54
40
  | Name | Possible values | Description | Default |
55
41
  | :------------- | --------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------- |
42
+ | `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load`
56
43
  | `raise_error:` | `false`, `true` | If set to true the middleware raises `OpenapiFirst::RequestInvalidError` instead of returning 4xx. | `false` (don't raise an exception) |
57
44
 
58
45
  The error responses conform with [JSON:API](https://jsonapi.org).
@@ -106,6 +93,56 @@ Request validation fails if request includes a property with `readOnly: true`.
106
93
 
107
94
  Response validation fails if response body includes a property with `writeOnly: true`.
108
95
 
96
+ ## OpenapiFirst::ResponseValidation
97
+
98
+ This middleware is especially useful when testing. It _always_ raises an error if the response is not valid.
99
+
100
+
101
+ ```ruby
102
+ use OpenapiFirst::ResponseValidation, spec: 'openapi.yaml' if ENV['RACK_ENV'] == 'test'
103
+ ```
104
+
105
+ ### Options
106
+
107
+ | Name | Possible values | Description | Default |
108
+ | :------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
109
+ | `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load`
110
+
111
+ ## OpenapiFirst::Router
112
+
113
+ This middleware first always used automatically, but you can add it to the top of your middleware stack if you want to change configuration.
114
+
115
+ ```ruby
116
+ use OpenapiFirst::Router, spec: './openapi/openapi.yaml'
117
+ ```
118
+
119
+ This middleware adds `env[OpenapiFirst::OPERATION]` which holds an Operation object that responds to `#operation_id`, `#path` (and `#[]` to access raw fields).
120
+
121
+ ### Options and defaults
122
+
123
+ | Name | Possible values | Description | Default |
124
+ | :------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
125
+ | `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load`
126
+ | |
127
+ | `raise_error:` | `false`, `true` | If set to true the middleware raises `OpenapiFirst::NotFoundError` when a path or method was not found in the API description. This is useful during testing to spot an incomplete API description. | `false` (don't raise an exception) |
128
+ | `not_found:` | `:continue`, `:halt` | If set to `:continue` the middleware will not return 404 (405, 415), but just pass handling the request to the next middleware or application in the Rack stack. If combined with `raise_error: true` `raise_error` gets preference and an exception is raised. | `:halt` (return 4xx response) |
129
+
130
+ ## OpenapiFirst::RackResponder
131
+
132
+ 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.
133
+ It does not not serialize objects as JSON or adds a content-type.
134
+
135
+ ```ruby
136
+ run OpenapiFirst::RackResponder
137
+ ```
138
+
139
+ ### Options
140
+
141
+ | Name | Description |
142
+ | :----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
143
+ | `namespace:` | Optional. A class or module where to find the handler method. |
144
+ | `resolver:` | Optional. An object that responds to `#call(operation)` and returns a [handler](#handlers). By default this is an instance of [DefaultOperationResolver](#OpenapiFirst::DefaultOperationResolver) |
145
+
109
146
  ## OpenapiFirst::Responder
110
147
 
111
148
  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.
@@ -129,7 +166,7 @@ It works like this:
129
166
 
130
167
  - An operationId "create_pet" or "createPet" or "create pet" calls `MyApi.create_pet(params, response)`
131
168
  - "some_things.create" calls: `MyApi::SomeThings.create(params, response)`
132
- - "pets#create" calls: `MyApi::Pets::Create.new.call(params, response)` If `MyApi::Pets::Create.new` accepts an argument, it will pass the rack `env`.
169
+ - "pets#create" instantiates the class once (`MyApi::Pets::Create.new) and calls it on every request(`instance.call(params, response)`).
133
170
 
134
171
  ### Handlers
135
172
 
@@ -145,14 +182,6 @@ There are two ways to set the response body:
145
182
  - Calling `res.write "things"` (see [Rack::Response](https://www.rubydoc.info/github/rack/rack/Rack/Response))
146
183
  - Returning a value which will get converted to JSON
147
184
 
148
- ## OpenapiFirst::ResponseValidation
149
-
150
- This middleware is especially useful when testing. It _always_ raises an error if the response is not valid.
151
-
152
- ```ruby
153
- use OpenapiFirst::ResponseValidation if ENV['RACK_ENV'] == 'test'
154
- ```
155
-
156
185
  ## Standalone usage
157
186
 
158
187
  Instead of composing these middlewares yourself you can use `OpenapiFirst.app`.
@@ -229,8 +258,7 @@ Instead of using the ResponseValidation middleware you can validate the response
229
258
  ```ruby
230
259
  # In your test (rspec example):
231
260
  require 'openapi_first'
232
- spec = OpenapiFirst.load('petstore.yaml')
233
- validator = OpenapiFirst::ResponseValidator.new(spec)
261
+ validator = OpenapiFirst::ResponseValidator.new('petstore.yaml')
234
262
 
235
263
  # This will raise an exception if it found an error
236
264
  validator.validate(last_request, last_response)
@@ -241,7 +269,7 @@ validator.validate(last_request, last_response)
241
269
  You can filter the URIs that should be handled by passing `only` to `OpenapiFirst.load`:
242
270
 
243
271
  ```ruby
244
- spec = OpenapiFirst.load './openapi/openapi.yaml', only: '/pets'.method(:==)
272
+ spec = OpenapiFirst.load('./openapi/openapi.yaml', only: '/pets'.method(:==))
245
273
  run OpenapiFirst.app(spec, namespace: Pets)
246
274
  ```
247
275
 
@@ -259,8 +287,7 @@ describe MyApp do
259
287
  include Rack::Test::Methods
260
288
 
261
289
  before(:all) do
262
- spec = OpenapiFirst.load('petstore.yaml')
263
- @app_wrapper = OpenapiFirst::Coverage.new(MyApp, spec)
290
+ @app_wrapper = OpenapiFirst::Coverage.new(MyApp, 'petstore.yaml')
264
291
  end
265
292
 
266
293
  after(:all) do
data/benchmarks/Gemfile CHANGED
@@ -10,6 +10,7 @@ gem 'hanami-api'
10
10
  gem 'hanami-router'
11
11
  gem 'multi_json'
12
12
  gem 'openapi_first', path: '../'
13
+ gem 'puma'
13
14
  gem 'roda'
14
15
  gem 'sinatra'
15
16
  gem 'syro'
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- openapi_first (0.17.0)
4
+ openapi_first (0.20.0)
5
5
  deep_merge (>= 1.2.1)
6
6
  hanami-router (= 2.0.alpha5)
7
7
  hanami-utils (= 2.0.alpha3)
@@ -72,7 +72,7 @@ GEM
72
72
  json_refs (0.1.7)
73
73
  hana
74
74
  json_schema (0.21.0)
75
- json_schemer (0.2.20)
75
+ json_schemer (0.2.22)
76
76
  ecma-re-validator (~> 0.3)
77
77
  hana (~> 1.3)
78
78
  regexp_parser (~> 2.0)
@@ -87,13 +87,16 @@ GEM
87
87
  mustermann (= 1.1.1)
88
88
  mustermann-grape (1.0.1)
89
89
  mustermann (>= 1.0.0)
90
+ nio4r (2.5.8)
90
91
  openapi_parser (0.15.0)
91
- rack (2.2.3)
92
+ puma (5.6.5)
93
+ nio4r (~> 2.0)
94
+ rack (2.2.3.1)
92
95
  rack-accept (0.4.5)
93
96
  rack (>= 0.4)
94
97
  rack-protection (2.2.0)
95
98
  rack
96
- regexp_parser (2.2.1)
99
+ regexp_parser (2.6.0)
97
100
  roda (3.54.0)
98
101
  rack
99
102
  ruby2_keywords (0.0.5)
@@ -113,6 +116,7 @@ GEM
113
116
 
114
117
  PLATFORMS
115
118
  arm64-darwin-21
119
+ x86_64-linux
116
120
 
117
121
  DEPENDENCIES
118
122
  benchmark-ips
@@ -123,6 +127,7 @@ DEPENDENCIES
123
127
  hanami-router
124
128
  multi_json
125
129
  openapi_first!
130
+ puma
126
131
  roda
127
132
  sinatra
128
133
  syro
@@ -0,0 +1,29 @@
1
+ # How to run these bechmarks
2
+
3
+ ## Setup
4
+
5
+ ```bash
6
+ cd benchmarks
7
+ bundle install
8
+ ```
9
+
10
+ ## Run Ruby benchmarks
11
+
12
+ This compares ips and memory usage for all apps defined in /apps
13
+
14
+ ```bash
15
+ bundle exec ruby benchmarks.rb
16
+ ```
17
+
18
+ ## Run benchmark using [wrk](https://github.com/wg/wrk)
19
+
20
+ 1. Start the example app
21
+ Example: openapi_first
22
+ ```bash
23
+ bundle exec puma apps/openapi_first_with_response_validation.ru
24
+ ```
25
+
26
+ 2. Run wrk
27
+ ```bash
28
+ ./benchmark-wrk.sh
29
+ ```
@@ -73,6 +73,16 @@ paths:
73
73
  operationId: create_thing
74
74
  description: Create a thing
75
75
  tags: ["Metadata"]
76
+ requestBody:
77
+ content:
78
+ application/json:
79
+ schema:
80
+ type: object
81
+ required:
82
+ - say
83
+ properties:
84
+ say:
85
+ type: string
76
86
  responses:
77
87
  "201":
78
88
  description: OK
@@ -84,3 +94,175 @@ paths:
84
94
  properties:
85
95
  hello:
86
96
  type: string
97
+ /pets:
98
+ get:
99
+ description: |
100
+ Returns all pets from the system that the user has access to
101
+ Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.
102
+
103
+ Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.
104
+ operationId: find_pets
105
+ parameters:
106
+ - name: tags
107
+ in: query
108
+ description: tags to filter by
109
+ required: false
110
+ style: form
111
+ schema:
112
+ type: array
113
+ items:
114
+ type: string
115
+ - name: limit
116
+ in: query
117
+ description: maximum number of results to return
118
+ required: false
119
+ schema:
120
+ type: integer
121
+ format: int32
122
+ responses:
123
+ '200':
124
+ description: pet response
125
+ content:
126
+ application/json:
127
+ schema:
128
+ type: array
129
+ items:
130
+ $ref: '#/components/schemas/Pet'
131
+ default:
132
+ description: unexpected error
133
+ content:
134
+ application/json:
135
+ schema:
136
+ $ref: '#/components/schemas/Error'
137
+ post:
138
+ description: Creates a new pet in the store. Duplicates are allowed
139
+ operationId: create_pet
140
+ requestBody:
141
+ description: Pet to add to the store
142
+ required: true
143
+ content:
144
+ application/json:
145
+ schema:
146
+ $ref: '#/components/schemas/NewPet'
147
+ responses:
148
+ '200':
149
+ description: pet response
150
+ content:
151
+ application/json:
152
+ schema:
153
+ $ref: '#/components/schemas/Pet'
154
+ default:
155
+ description: unexpected error
156
+ content:
157
+ application/json:
158
+ schema:
159
+ $ref: '#/components/schemas/Error'
160
+ /pets/{id}:
161
+ parameters:
162
+ - name: id
163
+ in: path
164
+ description: ID of pet to fetch
165
+ required: true
166
+ schema:
167
+ type: integer
168
+ format: int64
169
+ get:
170
+ description: Returns a user based on a single ID, if the user does not have access to the pet
171
+ operationId: find_pet
172
+ responses:
173
+ '200':
174
+ description: pet response
175
+ content:
176
+ application/json:
177
+ schema:
178
+ $ref: '#/components/schemas/Pet'
179
+ default:
180
+ description: unexpected error
181
+ content:
182
+ application/json:
183
+ schema:
184
+ $ref: '#/components/schemas/Error'
185
+ delete:
186
+ description: deletes a single pet based on the ID supplied
187
+ operationId: delete_pet
188
+ parameters:
189
+ - name: id
190
+ in: path
191
+ description: ID of pet to delete
192
+ required: true
193
+ schema:
194
+ type: integer
195
+ format: int64
196
+ responses:
197
+ '204':
198
+ description: pet deleted
199
+ default:
200
+ description: unexpected error
201
+ content:
202
+ application/json:
203
+ schema:
204
+ $ref: '#/components/schemas/Error'
205
+ patch:
206
+ description: Updates a pet
207
+ operationId: update_pet
208
+ requestBody:
209
+ description: Changes
210
+ required: false
211
+ content:
212
+ application/json:
213
+ schema:
214
+ $ref: '#/components/schemas/NewPet'
215
+ responses:
216
+ '200':
217
+ description: pet response
218
+ content:
219
+ application/json:
220
+ schema:
221
+ $ref: '#/components/schemas/Pet'
222
+ default:
223
+ description: unexpected error
224
+ content:
225
+ application/json:
226
+ schema:
227
+ $ref: '#/components/schemas/Error'
228
+
229
+ components:
230
+ schemas:
231
+ Pet:
232
+ allOf:
233
+ - $ref: '#/components/schemas/NewPet'
234
+ - required:
235
+ - id
236
+ properties:
237
+ id:
238
+ type: integer
239
+ format: int64
240
+
241
+ NewPet:
242
+ required:
243
+ - type
244
+ - attributes
245
+ properties:
246
+ type:
247
+ type: string
248
+ enum:
249
+ - pet
250
+ - plant
251
+ attributes:
252
+ additionalProperties: false
253
+ type: object
254
+ required: [name]
255
+ properties:
256
+ name:
257
+ type: string
258
+
259
+ Error:
260
+ required:
261
+ - code
262
+ - message
263
+ properties:
264
+ code:
265
+ type: integer
266
+ format: int32
267
+ message:
268
+ type: string
@@ -19,8 +19,7 @@ app = Class.new(Hanami::API) do
19
19
  end
20
20
  end.new
21
21
 
22
- oas_path = File.absolute_path('./openapi.yaml', __dir__)
23
- use OpenapiFirst::Router, spec: OpenapiFirst.load(oas_path)
22
+ use OpenapiFirst::Router, spec: File.absolute_path('./openapi.yaml', __dir__)
24
23
  use OpenapiFirst::RequestValidation
25
24
 
26
25
  run app
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ wrk -t12 -c400 -d10s --latency -s post.lua http://localhost:9292/hello
@@ -3,12 +3,16 @@
3
3
  require 'benchmark/ips'
4
4
  require 'benchmark/memory'
5
5
  require 'rack'
6
+ require 'json'
6
7
  ENV['RACK_ENV'] = 'production'
7
8
 
8
9
  examples = [
9
10
  [Rack::MockRequest.env_for('/hello'), 200],
10
11
  [Rack::MockRequest.env_for('/unknown'), 404],
11
- [Rack::MockRequest.env_for('/hello', method: 'POST'), 201],
12
+ [
13
+ Rack::MockRequest.env_for('/hello', method: 'POST', input: JSON.dump({ say: 'hi!' }),
14
+ 'CONTENT_TYPE' => 'application/json'), 201
15
+ ],
12
16
  [Rack::MockRequest.env_for('/hello/1'), 200],
13
17
  [Rack::MockRequest.env_for('/hello/123'), 200],
14
18
  [Rack::MockRequest.env_for('/hello?filter[id]=1,2'), 200]
@@ -24,7 +28,7 @@ bench = lambda do |app|
24
28
  env, expected_status = example
25
29
  100.times { app.call(env) }
26
30
  response = app.call(env)
27
- raise unless response[0] == expected_status
31
+ raise "expected status #{expected_status}, but was #{response[0]}" unless response[0] == expected_status
28
32
  end
29
33
  end
30
34
 
@@ -0,0 +1,3 @@
1
+ wrk.method = "POST"
2
+ wrk.body = "{\"say\":\"hi!\"}"
3
+ wrk.headers["Content-Type"] = "application/json"
@@ -10,11 +10,14 @@ module OpenapiFirst
10
10
  end
11
11
 
12
12
  def call(operation)
13
- @handlers[operation.name] ||= find_handler(operation['x-handler'] || operation['operationId'])
13
+ @handlers[operation.name] ||= begin
14
+ id = handler_id(operation)
15
+ find_handler(id) if id
16
+ end
14
17
  end
15
18
 
16
- def find_handler(operation_id)
17
- name = operation_id.match(/:*(.*)/)&.to_a&.at(1)
19
+ def find_handler(id)
20
+ name = id.match(/:*(.*)/)&.to_a&.at(1)
18
21
  return if name.nil?
19
22
 
20
23
  catch :halt do
@@ -27,6 +30,16 @@ module OpenapiFirst
27
30
  @namespace.method(method_name)
28
31
  end
29
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
+
30
43
  def find_class_method_handler(name)
31
44
  module_name, method_name = name.split('.')
32
45
  klass = find_const(@namespace, module_name)
@@ -37,9 +50,7 @@ module OpenapiFirst
37
50
  module_name, klass_name = name.split('#')
38
51
  const = find_const(@namespace, module_name)
39
52
  klass = find_const(const, klass_name)
40
- return ->(params, res) { klass.new.call(params, res) } if klass.instance_method(:initialize).arity.zero?
41
-
42
- ->(params, res) { klass.new(params.env).call(params, res) }
53
+ klass.new
43
54
  end
44
55
 
45
56
  def find_const(parent, name)
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ class Error < StandardError; end
5
+
6
+ class NotFoundError < Error; end
7
+
8
+ class HandlerNotFoundError < Error; end
9
+
10
+ class NotImplementedError < Error
11
+ def initialize(message)
12
+ warn 'NotImplementedError is deprecated. Handle HandlerNotFoundError instead'
13
+ super
14
+ end
15
+ end
16
+
17
+ class ResponseInvalid < Error; end
18
+
19
+ class ResponseCodeNotFoundError < ResponseInvalid; end
20
+
21
+ class ResponseContentTypeNotFoundError < ResponseInvalid; end
22
+
23
+ class ResponseBodyInvalidError < ResponseInvalid; end
24
+
25
+ class RequestInvalidError < Error
26
+ def initialize(serialized_errors)
27
+ message = error_message(serialized_errors)
28
+ super message
29
+ end
30
+
31
+ private
32
+
33
+ def error_message(errors)
34
+ errors.map do |error|
35
+ [human_source(error), human_error(error)].compact.join(' ')
36
+ end.join(', ')
37
+ end
38
+
39
+ def human_source(error)
40
+ return unless error[:source]
41
+
42
+ source_key = error[:source].keys.first
43
+ source = {
44
+ pointer: 'Request body invalid:',
45
+ parameter: 'Query parameter invalid:'
46
+ }.fetch(source_key, source_key)
47
+ name = error[:source].values.first
48
+ source += " #{name}" unless name.nil? || name.empty?
49
+ source
50
+ end
51
+
52
+ def human_error(error)
53
+ error[:title]
54
+ end
55
+ end
56
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- # An instance of this gets passed to handler functions as first argument.
4
+ # An instance of this gets passed to handler functions in the Responder.
5
5
  class Inbox < Hash
6
6
  attr_reader :env
7
7
 
@@ -47,9 +47,8 @@ module OpenapiFirst
47
47
  end
48
48
  end
49
49
 
50
- def content_type_for(status)
51
- content = response_for(status)['content']
52
- content.keys[0] if content
50
+ def content_types_for(status)
51
+ response_for(status)['content']&.keys
53
52
  end
54
53
 
55
54
  def response_schema_for(status, content_type)
@@ -94,6 +93,7 @@ module OpenapiFirst
94
93
  def response_by_code(status)
95
94
  operation_object.dig('responses', status.to_s) ||
96
95
  operation_object.dig('responses', "#{status / 100}XX") ||
96
+ operation_object.dig('responses', "#{status / 100}xx") ||
97
97
  operation_object.dig('responses', 'default')
98
98
  end
99
99
 
@@ -0,0 +1,12 @@
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
@@ -3,12 +3,12 @@
3
3
  require 'rack'
4
4
  require 'multi_json'
5
5
  require_relative 'inbox'
6
- require_relative 'router_required'
6
+ require_relative 'use_router'
7
7
  require_relative 'validation_format'
8
8
 
9
9
  module OpenapiFirst
10
10
  class RequestValidation # rubocop:disable Metrics/ClassLength
11
- prepend RouterRequired
11
+ prepend UseRouter
12
12
 
13
13
  def initialize(app, options = {})
14
14
  @app = app
@@ -16,10 +16,10 @@ module OpenapiFirst
16
16
  end
17
17
 
18
18
  def call(env) # rubocop:disable Metrics/AbcSize
19
- operation = env[OpenapiFirst::OPERATION]
19
+ operation = env[OPERATION]
20
20
  return @app.call(env) unless operation
21
21
 
22
- env[INBOX] = Inbox.new(env)
22
+ env[INBOX] = {}
23
23
  catch(:halt) do
24
24
  validate_query_parameters!(env, operation, env[PARAMETERS])
25
25
  req = Rack::Request.new(env)
@@ -16,14 +16,18 @@ module OpenapiFirst
16
16
  operation = env[OpenapiFirst::OPERATION]
17
17
  res = Rack::Response.new
18
18
  handler = find_handler(operation)
19
- result = handler.call(env[INBOX], res)
19
+ result = handler.call(inbox(env), res)
20
20
  res.write serialize(result) if result && res.body.empty?
21
- res[Rack::CONTENT_TYPE] ||= operation.content_type_for(res.status)
21
+ res[Rack::CONTENT_TYPE] ||= operation.content_types_for(res.status)&.first
22
22
  res.finish
23
23
  end
24
24
 
25
25
  private
26
26
 
27
+ def inbox(env)
28
+ Inbox.new(env).tap { |i| i.merge!(env[INBOX]) if env[INBOX] }
29
+ end
30
+
27
31
  def find_handler(operation)
28
32
  handler = @resolver.call(operation)
29
33
  raise NotImplementedError, "Could not find handler for #{operation.name}" unless handler
@@ -37,11 +41,4 @@ module OpenapiFirst
37
41
  MultiJson.dump(result)
38
42
  end
39
43
  end
40
-
41
- class OperationResolver < Responder
42
- def initialize(spec:, namespace:)
43
- warn "#{self.class.name} was renamed to #{OpenapiFirst::Responder.name}"
44
- super
45
- end
46
- end
47
44
  end
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'multi_json'
4
- require_relative 'router_required'
4
+ require_relative 'use_router'
5
5
  require_relative 'validation_format'
6
6
 
7
7
  module OpenapiFirst
8
8
  class ResponseValidation
9
- prepend RouterRequired
9
+ prepend UseRouter
10
10
 
11
- def initialize(app)
11
+ def initialize(app, _options = {})
12
12
  @app = app
13
13
  end
14
14
 
@@ -42,20 +42,21 @@ module OpenapiFirst
42
42
  data = full_body.empty? ? {} : load_json(full_body)
43
43
  errors = schema.validate(data)
44
44
  errors = errors.to_a.map! do |error|
45
- error_message_for(error)
45
+ format_error(error)
46
46
  end
47
47
  raise ResponseBodyInvalidError, errors.join(', ') if errors.any?
48
48
  end
49
49
 
50
+ def format_error(error)
51
+ return "Write-only field appears in response: #{error['data_pointer']}" if error['type'] == 'writeOnly'
52
+
53
+ JSONSchemer::Errors.pretty(error)
54
+ end
55
+
50
56
  def load_json(string)
51
57
  MultiJson.load(string)
52
58
  rescue MultiJson::ParseError
53
59
  string
54
60
  end
55
-
56
- def error_message_for(error)
57
- err = ValidationFormat.error_details(error)
58
- [err[:title], error['data_pointer'], err[:detail]].compact.join(' ')
59
- end
60
61
  end
61
62
  end
@@ -14,6 +14,10 @@ module OpenapiFirst
14
14
  @raise = options.fetch(:raise_error, false)
15
15
  @not_found = options.fetch(:not_found, :halt)
16
16
  spec = options.fetch(:spec)
17
+ raise "You have to pass spec: when initializing #{self.class}" unless spec
18
+
19
+ spec = OpenapiFirst.load(spec) unless spec.is_a?(Definition)
20
+
17
21
  @filepath = spec.filepath
18
22
  @router = build_router(spec.operations)
19
23
  end
@@ -28,6 +32,7 @@ module OpenapiFirst
28
32
 
29
33
  return @app.call(env) if @not_found == :continue
30
34
  end
35
+
31
36
  response
32
37
  end
33
38
 
@@ -53,13 +58,10 @@ module OpenapiFirst
53
58
  env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
54
59
  end
55
60
 
56
- def build_router(operations) # rubocop:disable Metrics/AbcSize
61
+ def build_router(operations)
57
62
  router = Hanami::Router.new
58
63
  operations.each do |operation|
59
64
  normalized_path = operation.path.gsub('{', ':').gsub('}', '')
60
- if operation.operation_id.nil?
61
- warn "operationId is missing in '#{operation.method} #{operation.path}'. I am ignoring this operation."
62
- end
63
65
  router.public_send(
64
66
  operation.method,
65
67
  normalized_path,
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ module UseRouter
5
+ def initialize(app, options = {})
6
+ @app = app
7
+ @options = options
8
+ super
9
+ end
10
+
11
+ def call(env)
12
+ return super if env.key?(OPERATION)
13
+
14
+ @router ||= Router.new(lambda { |e|
15
+ super(e)
16
+ }, spec: @options.fetch(:spec), raise_error: @options.fetch(:raise_error, false))
17
+ @router.call(env)
18
+ end
19
+ end
20
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.17.0'
4
+ VERSION = '0.20.0'
5
5
  end
data/lib/openapi_first.rb CHANGED
@@ -4,6 +4,7 @@ require 'yaml'
4
4
  require 'json_refs'
5
5
  require_relative 'openapi_first/definition'
6
6
  require_relative 'openapi_first/version'
7
+ require_relative 'openapi_first/errors'
7
8
  require_relative 'openapi_first/inbox'
8
9
  require_relative 'openapi_first/router'
9
10
  require_relative 'openapi_first/request_validation'
@@ -20,7 +21,7 @@ module OpenapiFirst
20
21
  HANDLER = 'openapi_first.handler'
21
22
 
22
23
  def self.env
23
- ENV['RACK_ENV'] || ENV['HANAMI_ENV'] || ENV['RAILS_ENV']
24
+ ENV['RACK_ENV'] || ENV['HANAMI_ENV'] || ENV.fetch('RAILS_ENV', nil)
24
25
  end
25
26
 
26
27
  def self.load(spec_path, only: nil)
@@ -39,7 +40,7 @@ module OpenapiFirst
39
40
  request_validation_raise_error: false,
40
41
  response_validation: false
41
42
  )
42
- spec = OpenapiFirst.load(spec) if spec.is_a?(String)
43
+ spec = OpenapiFirst.load(spec) unless spec.is_a?(Definition)
43
44
  App.new(
44
45
  nil,
45
46
  spec,
@@ -57,7 +58,7 @@ module OpenapiFirst
57
58
  request_validation_raise_error: false,
58
59
  response_validation: false
59
60
  )
60
- spec = OpenapiFirst.load(spec) if spec.is_a?(String)
61
+ spec = OpenapiFirst.load(spec) unless spec.is_a?(Definition)
61
62
  AppWithOptions.new(
62
63
  spec,
63
64
  namespace: namespace,
@@ -77,50 +78,4 @@ module OpenapiFirst
77
78
  App.new(app, @spec, **@options)
78
79
  end
79
80
  end
80
-
81
- class Error < StandardError; end
82
-
83
- class NotFoundError < Error; end
84
-
85
- class NotImplementedError < RuntimeError; end
86
-
87
- class ResponseInvalid < Error; end
88
-
89
- class ResponseCodeNotFoundError < ResponseInvalid; end
90
-
91
- class ResponseContentTypeNotFoundError < ResponseInvalid; end
92
-
93
- class ResponseBodyInvalidError < ResponseInvalid; end
94
-
95
- class RequestInvalidError < Error
96
- def initialize(serialized_errors)
97
- message = error_message(serialized_errors)
98
- super message
99
- end
100
-
101
- private
102
-
103
- def error_message(errors)
104
- errors.map do |error|
105
- [human_source(error), human_error(error)].compact.join(' ')
106
- end.join(', ')
107
- end
108
-
109
- def human_source(error)
110
- return unless error[:source]
111
-
112
- source_key = error[:source].keys.first
113
- source = {
114
- pointer: 'Request body invalid:',
115
- parameter: 'Query parameter invalid:'
116
- }.fetch(source_key, source_key)
117
- name = error[:source].values.first
118
- source += " #{name}" unless name.nil? || name.empty?
119
- source
120
- end
121
-
122
- def human_error(error)
123
- error[:title]
124
- end
125
- end
126
81
  end
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.17.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-04-11 00:00:00.000000000 Z
11
+ date: 2022-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deep_merge
@@ -190,6 +190,7 @@ files:
190
190
  - Rakefile
191
191
  - benchmarks/Gemfile
192
192
  - benchmarks/Gemfile.lock
193
+ - benchmarks/README.md
193
194
  - benchmarks/apps/committee.ru
194
195
  - benchmarks/apps/committee_with_response_validation.ru
195
196
  - benchmarks/apps/committee_with_sinatra.ru
@@ -203,7 +204,9 @@ files:
203
204
  - benchmarks/apps/roda.ru
204
205
  - benchmarks/apps/sinatra.ru
205
206
  - benchmarks/apps/syro.ru
207
+ - benchmarks/benchmark-wrk.sh
206
208
  - benchmarks/benchmarks.rb
209
+ - benchmarks/post.lua
207
210
  - bin/console
208
211
  - bin/setup
209
212
  - examples/README.md
@@ -215,16 +218,18 @@ files:
215
218
  - lib/openapi_first/coverage.rb
216
219
  - lib/openapi_first/default_operation_resolver.rb
217
220
  - lib/openapi_first/definition.rb
221
+ - lib/openapi_first/errors.rb
218
222
  - lib/openapi_first/inbox.rb
219
223
  - lib/openapi_first/operation.rb
224
+ - lib/openapi_first/rack_responder.rb
220
225
  - lib/openapi_first/request_validation.rb
221
226
  - lib/openapi_first/responder.rb
222
227
  - lib/openapi_first/response_object.rb
223
228
  - lib/openapi_first/response_validation.rb
224
229
  - lib/openapi_first/response_validator.rb
225
230
  - lib/openapi_first/router.rb
226
- - lib/openapi_first/router_required.rb
227
231
  - lib/openapi_first/schema_validation.rb
232
+ - lib/openapi_first/use_router.rb
228
233
  - lib/openapi_first/utils.rb
229
234
  - lib/openapi_first/validation.rb
230
235
  - lib/openapi_first/validation_format.rb
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OpenapiFirst
4
- module RouterRequired
5
- def call(env)
6
- unless env.key?(OPERATION)
7
- raise 'OpenapiFirst::Router missing in middleware stack. Did you forget adding OpenapiFirst::Router?'
8
- end
9
-
10
- super
11
- end
12
- end
13
- end