openapi_first 0.6.1 → 0.6.3

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: 6888d00f37a7dce1f2b22ad9da04f4ecb69236ea18221173fc77dac1b5edba6c
4
- data.tar.gz: 8eca9351e46942b1acf6fa47d3394dbfa9a85c4f0dc3dc1f77c075d2818bc3ed
3
+ metadata.gz: 9c39942d9c5860a92b211d51852ad5633dd573f2a244d85631d2c2c4990e607c
4
+ data.tar.gz: 8bbfd7a2cdf632338117d689dd770561d5f4a903223800c9a6829e556ab1c26a
5
5
  SHA512:
6
- metadata.gz: 967dd87e43808f328a5ab72e99cf04c4b37eb756b2a4ef06925e3e419b038583e014cf3c11074168f02fd07b16db4ba266f743a884a608f37e6c1bfeed0ce911
7
- data.tar.gz: 9c2a62d290c09417c0c75eaec88427b847f43d787e3cb548dcef543749a98f7252d5915de48fe64c9eb34eca8f579590f1d94bec9455c1ba08932b1a5de5c7f4
6
+ metadata.gz: 58ab052d0d464da3f37224c943a9da8c990eb6f75b13306b223d1a6d4e36f55588df3e5f2106ca2659a44a048604cd011da50d6b11417efb8b45f05762ab23e4
7
+ data.tar.gz: 53ba1d5a794cd1561e812c37cc426e6d282bbfd6c7bb5cce547be7eda1d90c342d4b6cdfc490e1b0765b43ba7fba126abea8ae413c0744c444c2c2aae725b2aa
@@ -1,10 +1,21 @@
1
- # Unreleased
1
+ # Changelog
2
2
 
3
- # 0.6.1
3
+ ## Unreleased
4
+
5
+
6
+ ## 0.6.3
7
+
8
+ - Add option to parse only certain paths from OAS file
9
+
10
+ ## 0.6.2
11
+
12
+ - Add support to map operationIds like `things#index` or `web.things_index`
13
+
14
+ ## 0.6.1
4
15
 
5
16
  - Make ResponseValidator errors easier to read
6
17
 
7
- # 0.6.0
18
+ ## 0.6.0
8
19
 
9
20
  - Set the content-type based on the OpenAPI description [#29](https://github.com/ahx/openapi-first/pull/29)
10
21
  - Add CHANGELOG 📝
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (0.6.1)
4
+ openapi_first (0.6.3)
5
5
  json_schemer (~> 0.2)
6
6
  multi_json (~> 1.13)
7
7
  oas_parser (~> 0.19)
@@ -10,13 +10,14 @@ PATH
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
- activesupport (5.2.3)
13
+ activesupport (6.0.0)
14
14
  concurrent-ruby (~> 1.0, >= 1.0.2)
15
15
  i18n (>= 0.7, < 2)
16
16
  minitest (~> 5.1)
17
17
  tzinfo (~> 1.1)
18
- addressable (2.6.0)
19
- public_suffix (>= 2.0.2, < 4.0)
18
+ zeitwerk (~> 2.1, >= 2.1.8)
19
+ addressable (2.7.0)
20
+ public_suffix (>= 2.0.2, < 5.0)
20
21
  ast (2.4.0)
21
22
  builder (3.2.3)
22
23
  coderay (1.1.2)
@@ -27,29 +28,31 @@ GEM
27
28
  regexp_parser (~> 1.2)
28
29
  hana (1.3.5)
29
30
  hansi (0.2.0)
30
- i18n (1.6.0)
31
+ hash-deep-merge (0.1.1)
32
+ i18n (1.7.0)
31
33
  concurrent-ruby (~> 1.0)
32
34
  jaro_winkler (1.5.3)
33
- json_schemer (0.2.0)
34
- ecma-re-validator (~> 0.2.0)
35
- hana (~> 1.3.3)
36
- regexp_parser (~> 1.2.0)
37
- uri_template (~> 0.7.0)
35
+ json_schemer (0.2.7)
36
+ ecma-re-validator (~> 0.2)
37
+ hana (~> 1.3)
38
+ regexp_parser (~> 1.5)
39
+ uri_template (~> 0.7)
38
40
  method_source (0.9.2)
39
41
  mini_portile2 (2.4.0)
40
- minitest (5.11.3)
41
- multi_json (1.13.1)
42
+ minitest (5.12.2)
43
+ multi_json (1.14.1)
42
44
  mustermann (1.0.3)
43
45
  mustermann-contrib (1.0.3)
44
46
  hansi (~> 0.2.0)
45
47
  mustermann (= 1.0.3)
46
- nokogiri (1.10.3)
48
+ nokogiri (1.10.4)
47
49
  mini_portile2 (~> 2.4.0)
48
- oas_parser (0.19.0)
50
+ oas_parser (0.22.2)
49
51
  activesupport (>= 4.0.0)
50
52
  addressable (~> 2.3)
51
53
  builder (~> 3.2.3)
52
54
  deep_merge (~> 1.2.1)
55
+ hash-deep-merge
53
56
  mustermann-contrib (~> 1.0.3s)
54
57
  nokogiri
55
58
  parallel (1.17.0)
@@ -58,13 +61,13 @@ GEM
58
61
  pry (0.12.2)
59
62
  coderay (~> 1.1.0)
60
63
  method_source (~> 0.9.0)
61
- public_suffix (3.1.1)
64
+ public_suffix (4.0.1)
62
65
  rack (2.0.7)
63
66
  rack-test (1.1.0)
64
67
  rack (>= 1.0, < 3)
65
68
  rainbow (3.0.0)
66
69
  rake (10.5.0)
67
- regexp_parser (1.2.0)
70
+ regexp_parser (1.6.0)
68
71
  rspec (3.8.0)
69
72
  rspec-core (~> 3.8.0)
70
73
  rspec-expectations (~> 3.8.0)
@@ -91,6 +94,7 @@ GEM
91
94
  thread_safe (~> 0.1)
92
95
  unicode-display_width (1.6.0)
93
96
  uri_template (0.7.0)
97
+ zeitwerk (2.2.0)
94
98
 
95
99
  PLATFORMS
96
100
  ruby
@@ -105,4 +109,4 @@ DEPENDENCIES
105
109
  rubocop
106
110
 
107
111
  BUNDLED WITH
108
- 2.0.1
112
+ 2.0.2
data/README.md CHANGED
@@ -1,9 +1,15 @@
1
1
  # OpenapiFirst
2
2
 
3
- OpenapiFirst helps to implement HTTP APIs based on an [OpenApi](https://www.openapis.org/) API description. The idea is that you create an API description first, then add minimal code about your business logic (some call this "Resolver") and be done.
3
+ OpenapiFirst helps to implement HTTP APIs based on an [OpenApi](https://www.openapis.org/) API description. The idea is that you create an API description first, then add minimal code about your business logic (some call this "handler") and be done.
4
4
 
5
5
  ## TL;DR
6
6
 
7
+ Start with writing an OpenAPI file that describes the API, which you are about to write. Use a [validator](http://speccy.io/) to make sure the file is valid.
8
+
9
+ We recommend saving the file as `openapi/openapi.yaml`.
10
+
11
+ Now implement your API:
12
+
7
13
  ```ruby
8
14
  module Pets
9
15
  def self.find_pet(params, res)
@@ -21,7 +27,7 @@ run OpenapiFirst.app('./openapi/openapi.yaml', namespace: Pets)
21
27
 
22
28
  The above will:
23
29
 
24
- - Validate the request and respond with 400 if the request does not match against your spec
30
+ - Validate the request and respond with 400 if the request does not match with your API description
25
31
  - Map the request to a method call `Pets.find_pet` based on the `operationId` in the API description
26
32
  - Set the response content type according to your spec (here with the default status code `200`)
27
33
 
@@ -31,6 +37,17 @@ Resolver functions (`find_pet`) are called with two arguments:
31
37
  - `res` - Holds a Rack::Response that you can modify if needed
32
38
  If you want to access to plain Rack env you can call `params.env`.
33
39
 
40
+ You can also use the provided Rack middlewares to auto-implement only certain aspects of the request-response flow like query parameter or request body parameter validation based on your OpenAPI file. Read on to learn how.
41
+
42
+ ### Handling only certain paths
43
+
44
+ You can filter the URIs that should be handled by pass ing `only` to `OpenapiFirst.load`:
45
+
46
+ ```ruby
47
+ spec = OpenapiFirst.load './openapi/openapi.yaml', only: '/pets'.method(:==)
48
+ run OpenapiFirst.app(spec, namespace: Pets)
49
+ ```
50
+
34
51
  ### Usage as Rack middleware
35
52
 
36
53
  ```ruby
@@ -45,15 +62,6 @@ When using the middleware, all requests that are not part of the API description
45
62
 
46
63
  See [example](examples)
47
64
 
48
- ## Missing features
49
-
50
- See [issues](https://github.com/ahx/openapi_first/issues).
51
-
52
- ## Start
53
-
54
- Start with writing an OpenAPI file that describes the API, which you are about to write. Use a [validator](http://speccy.io/) to make sure the file is valid.
55
-
56
- We recommend saving the file as `openapi/openapi.yaml`.
57
65
 
58
66
  ## Installation
59
67
 
@@ -65,76 +73,15 @@ gem 'openapi_first'
65
73
 
66
74
  OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
67
75
 
68
- ## Testing
69
-
70
- OpenapiFirst offers tools to help testing your app against your API description.
71
-
72
- ### Response validation
73
-
74
- Response validation is to make sure your app responds as described in your API description. You usually do this in your tests using [rack-test](https://github.com/rack-test/rack-test).
75
-
76
- ```ruby
77
- # In your test:
78
- require 'openapi_first/response_validator'
79
- spec = OpenapiFirst.load('petstore.yaml')
80
- validator = OpenapiFirst::ResponseValidator.new(spec)
81
- validator.validate(last_request, last_response).errors? # => true or false
82
- ```
83
-
84
- TODO: Add RSpec matcher (via extra rubygem)
85
-
86
- ### Coverage
87
-
88
- (This is a bit experimental. Please try it out and give feedback.)
89
-
90
- `OpenapiFirst::Coverage` helps you make sure, that you have called all endpoints of your OAS file when running tests via `rack-test`.
91
-
92
- ```ruby
93
- # In your test (rspec example):
94
- require 'openapi_first/coverage'
95
-
96
- describe MyApp do
97
- include Rack::Test::Methods
98
-
99
- before(:all) do
100
- spec = OpenapiFirst.load('petstore.yaml')
101
- @app_wrapper = OpenapiFirst::Coverage.new(MyApp, spec)
102
- end
103
-
104
- after(:all) do
105
- message = "The following paths have not been called yet: #{@app_wrapper.to_be_called}"
106
- expect(@app_wrapper.to_be_called).to be_empty
107
- end
108
-
109
- # Overwrite `#app` to make rack-test call the wrapped app
110
- def app
111
- @app_wrapper
112
- end
113
-
114
- it 'does things' do
115
- get '/i/my/stuff'
116
- # …
117
- end
118
- end
119
- ```
120
-
121
- ## Mocking
122
-
123
- Mocking is currently out of scope. Try https://github.com/JustinFeng/fakeit or something else.
124
-
125
- ## Alternatives
126
-
127
- This gem is inspired by [committee](https://github.com/interagent/committee), which has much more features like response stubs or support for Hyper-Schema or OpenAPI 2.
128
-
129
76
  ## How it works
130
77
 
131
- OpenapiFirst offers Rack middlewares to auto-implement different aspects of request validation:
78
+ OpenapiFirst offers Rack middlewares to auto-implement different aspects for request handling:
132
79
 
133
80
  - Query parameter validation
134
81
  - Request body validation
135
82
  - Mapping request to a function call
136
83
 
137
- It starts with a router middleware:
84
+ It starts by adding a router middleware:
138
85
 
139
86
  ```ruby
140
87
  spec = OpenapiFirst.load('petstore.yaml')
@@ -163,7 +110,7 @@ content-type: "application/vnd.api+json"
163
110
  }
164
111
  ```
165
112
 
166
- ### Query parameter validation
113
+ ## Query parameter validation
167
114
 
168
115
  ```ruby
169
116
  use OpenapiFirst::QueryParameterValidation
@@ -181,11 +128,11 @@ If you want to forbid nested query parameters you will need to use `additionalPr
181
128
 
182
129
  OpenapiFirst does not support parameters set to `explode: false` and treats nested query parameters (`filter[foo]=bar`) like [`style: deepObject`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#style-values).
183
130
 
184
- ### TODO: Header, Cookie, Path parameter validation
131
+ ## Header, Cookie, Path parameter validation
185
132
 
186
133
  tbd.
187
134
 
188
- ### Request Body validation
135
+ ## Request Body validation
189
136
 
190
137
  ```ruby
191
138
  # Add the middleware:
@@ -197,14 +144,34 @@ This will add the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
197
144
 
198
145
  OpenAPI request (and response) body validation is based on [JSON Schema](http://json-schema.org/).
199
146
 
200
- ### Mapping request to a function call
147
+ ## Mapping the request to a method call
148
+
149
+ OpenapiFirst uses a `OperationResolver` middleware to map the HTTP request to a method call.
150
+
151
+ The resolver function is found via the [`operationId`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operation-object) attribute in your API description like this:
152
+
153
+ - `create_pet` will map to `MyApi.create_pet(params, response)`
154
+ - `some_things.create` will map to `MyApi::SomeThings.create(params, response)`
155
+ - `pets#create` will map to `MyApi::Pets::Create.new.call(params, response)` (like [Hanami::Router](https://github.com/hanami/router#controllers))
156
+
157
+ These handler methods are called with two arguments:
201
158
 
202
- OpenapiFirst has a `OperationResolver` middleware to map the HTTP request to a function (method) call
159
+ - `params` - Holds the parsed request body, filtered query params and path parameters
160
+ - `res` - Holds a Rack::Response that you can modify if needed
161
+
162
+ You can call `params.env` to access the Rack env (just like in [Hanami actions](https://guides.hanamirb.org/actions/parameters/))
163
+
164
+ There are two ways to set the response body:
165
+
166
+ - Calling `res.write "things"` (see [Rack::Response](https://www.rubydoc.info/github/rack/rack/Rack/Response))
167
+ - Returning a value from the function (see example above) (this will always converted to JSON)
168
+
169
+ ### Adding the middleware
203
170
 
204
171
  ```ruby
205
172
  # Define some methods
206
173
  module MyApi
207
- def create_pet(params, res)
174
+ def self.create_pet(params, res)
208
175
  res.status = 201
209
176
  {
210
177
  id: '1',
@@ -225,40 +192,20 @@ run OpenapiFirst::OperationResolver, namespace: Pets
225
192
  # POST /pets, { name: 'Oscar' }
226
193
  ```
227
194
 
228
- The resolver function is found via the [`operationId`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operation-object) attribute in your API description. If your operationId has dots like `Pets.find`, the resolver above would call `MyApi::Pets.find(params, req)`.
195
+ ## Response validation
229
196
 
230
- These resolver functions are called with two arguments:
231
-
232
- - `params` - Holds the parsed request body, filtered query params and path parameters
233
- - `res` - Holds a Rack::Response that you can modify if needed
234
-
235
- You can call `params.env` to access the Rack env (just like in [Hanami actions](https://guides.hanamirb.org/actions/parameters/))
236
-
237
- There are two ways to set the response body:
238
-
239
- - Calling `res.write "things"` (see [Rack::Response](https://www.rubydoc.info/github/rack/rack/Rack/Response))
240
- - Returning a value from the function (see example above) (this will always converted to JSON)
241
-
242
- ## Testing
243
-
244
- OpenapiFirst offers tools to help testing your app.
245
-
246
- ### Response validation
247
-
248
- Response validation is to make sure your app responds as described in your OpenAPI spec. You usually do this in your tests using [rack-test](https://github.com/rack-test/rack-test).
197
+ Response validation is useful to make sure your app responds as described in your API description. You usually do this in your tests using [rack-test](https://github.com/rack-test/rack-test).
249
198
 
250
199
  ```ruby
251
- # In your test:
252
- require 'openapi_first/response_validator'
200
+ # In your test (rspec example):
201
+ require 'openapi_first'
253
202
  spec = OpenapiFirst.load('petstore.yaml')
254
203
  validator = OpenapiFirst::ResponseValidator.new(spec)
255
204
 
256
205
  expect(validator.validate(last_request, last_response).errors).to be_empty
257
206
  ```
258
207
 
259
- TODO: Add RSpec matcher (via extra rubygem)
260
-
261
- ### Coverage
208
+ ## Coverage
262
209
 
263
210
  (This is a bit experimental. Please try it out and give feedback.)
264
211
 
@@ -307,6 +254,16 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
307
254
 
308
255
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
309
256
 
257
+ ### Run benchmarks
258
+
259
+ ```sh
260
+ cd benchmarks
261
+ bundle
262
+ bundle exec ruby benchmarks.rb
263
+ ```
264
+
310
265
  ## Contributing
311
266
 
312
- Bug reports and pull requests are welcome on GitHub at https://github.com/ahx/openapi_first.
267
+ If you have a question or an idea or found a bug don't hesitate to [create an issue on GitHub](https://github.com/ahx/openapi_first/issues).
268
+
269
+ Pull requests are very welcome as well, of course. Feel free to create a "draft" pull request early on, even if your change is still work in progress. 🤗
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'benchmark-ips'
6
+ gem 'benchmark-memory'
7
+ gem 'committee'
8
+ gem 'grape'
9
+ gem 'hanami-router', '~> 2.0.0.alpha1'
10
+ gem 'multi_json'
11
+ gem 'openapi_first', path: '../'
12
+ gem 'sinatra'
13
+ gem 'syro'
@@ -0,0 +1,136 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ openapi_first (0.6.3)
5
+ json_schemer (~> 0.2)
6
+ multi_json (~> 1.13)
7
+ oas_parser (~> 0.19)
8
+ rack (~> 2)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ activesupport (6.0.0)
14
+ concurrent-ruby (~> 1.0, >= 1.0.2)
15
+ i18n (>= 0.7, < 2)
16
+ minitest (~> 5.1)
17
+ tzinfo (~> 1.1)
18
+ zeitwerk (~> 2.1, >= 2.1.8)
19
+ addressable (2.7.0)
20
+ public_suffix (>= 2.0.2, < 5.0)
21
+ axiom-types (0.1.1)
22
+ descendants_tracker (~> 0.0.4)
23
+ ice_nine (~> 0.11.0)
24
+ thread_safe (~> 0.3, >= 0.3.1)
25
+ benchmark-ips (2.7.2)
26
+ benchmark-memory (0.1.2)
27
+ memory_profiler (~> 0.9)
28
+ builder (3.2.3)
29
+ coercible (1.0.0)
30
+ descendants_tracker (~> 0.0.1)
31
+ committee (3.2.1)
32
+ json_schema (~> 0.14, >= 0.14.3)
33
+ openapi_parser (>= 0.6.1)
34
+ rack (>= 1.5)
35
+ concurrent-ruby (1.1.5)
36
+ deep_merge (1.2.1)
37
+ descendants_tracker (0.0.4)
38
+ thread_safe (~> 0.3, >= 0.3.1)
39
+ dry-inflector (0.2.0)
40
+ ecma-re-validator (0.2.0)
41
+ regexp_parser (~> 1.2)
42
+ equalizer (0.0.11)
43
+ grape (1.2.4)
44
+ activesupport
45
+ builder
46
+ mustermann-grape (~> 1.0.0)
47
+ rack (>= 1.3.0)
48
+ rack-accept
49
+ virtus (>= 1.0.0)
50
+ hana (1.3.5)
51
+ hanami-router (2.0.0.alpha1)
52
+ dry-inflector (~> 0.1)
53
+ hanami-utils (~> 2.0.alpha)
54
+ mustermann (~> 1.0)
55
+ mustermann-contrib (~> 1.0)
56
+ rack (~> 2.0)
57
+ hanami-utils (2.0.0.alpha1)
58
+ concurrent-ruby (~> 1.0)
59
+ transproc (~> 1.0)
60
+ hansi (0.2.0)
61
+ hash-deep-merge (0.1.1)
62
+ i18n (1.7.0)
63
+ concurrent-ruby (~> 1.0)
64
+ ice_nine (0.11.2)
65
+ json_schema (0.20.8)
66
+ json_schemer (0.2.7)
67
+ ecma-re-validator (~> 0.2)
68
+ hana (~> 1.3)
69
+ regexp_parser (~> 1.5)
70
+ uri_template (~> 0.7)
71
+ memory_profiler (0.9.14)
72
+ mini_portile2 (2.4.0)
73
+ minitest (5.12.2)
74
+ multi_json (1.14.1)
75
+ mustermann (1.0.3)
76
+ mustermann-contrib (1.0.3)
77
+ hansi (~> 0.2.0)
78
+ mustermann (= 1.0.3)
79
+ mustermann-grape (1.0.0)
80
+ mustermann (~> 1.0.0)
81
+ nokogiri (1.10.4)
82
+ mini_portile2 (~> 2.4.0)
83
+ oas_parser (0.22.2)
84
+ activesupport (>= 4.0.0)
85
+ addressable (~> 2.3)
86
+ builder (~> 3.2.3)
87
+ deep_merge (~> 1.2.1)
88
+ hash-deep-merge
89
+ mustermann-contrib (~> 1.0.3s)
90
+ nokogiri
91
+ openapi_parser (0.6.1)
92
+ public_suffix (4.0.1)
93
+ rack (2.0.7)
94
+ rack-accept (0.4.5)
95
+ rack (>= 0.4)
96
+ rack-protection (2.0.7)
97
+ rack
98
+ regexp_parser (1.6.0)
99
+ seg (1.2.0)
100
+ sinatra (2.0.7)
101
+ mustermann (~> 1.0)
102
+ rack (~> 2.0)
103
+ rack-protection (= 2.0.7)
104
+ tilt (~> 2.0)
105
+ syro (3.1.1)
106
+ rack (>= 1.6.0)
107
+ seg
108
+ thread_safe (0.3.6)
109
+ tilt (2.0.10)
110
+ transproc (1.1.0)
111
+ tzinfo (1.2.5)
112
+ thread_safe (~> 0.1)
113
+ uri_template (0.7.0)
114
+ virtus (1.0.5)
115
+ axiom-types (~> 0.1)
116
+ coercible (~> 1.0)
117
+ descendants_tracker (~> 0.0, >= 0.0.3)
118
+ equalizer (~> 0.0, >= 0.0.9)
119
+ zeitwerk (2.2.0)
120
+
121
+ PLATFORMS
122
+ ruby
123
+
124
+ DEPENDENCIES
125
+ benchmark-ips
126
+ benchmark-memory
127
+ committee
128
+ grape
129
+ hanami-router (~> 2.0.0.alpha1)
130
+ multi_json
131
+ openapi_first!
132
+ sinatra
133
+ syro
134
+
135
+ BUNDLED WITH
136
+ 2.0.2
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'committee'
4
+ require 'syro'
5
+ require 'multi_json'
6
+
7
+ app = Syro.new do
8
+ on 'hello' do
9
+ on :id do
10
+ get do
11
+ res.json MultiJson.dump(hello: 'world', id: inbox[:id])
12
+ end
13
+ end
14
+
15
+ get do
16
+ res.json [MultiJson.dump(hello: 'world')]
17
+ end
18
+
19
+ post do
20
+ res.status = 201
21
+ res.json MultiJson.dump(hello: 'world')
22
+ end
23
+ end
24
+ end
25
+
26
+ use Committee::Middleware::RequestValidation,
27
+ schema_path: './apps/openapi.yaml',
28
+ coerce_date_times: false
29
+
30
+ run app
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'grape'
4
+
5
+ class GrapeExample < Grape::API
6
+ format :json
7
+
8
+ get :hello do
9
+ [{ hello: 'world' }]
10
+ end
11
+
12
+ post :hello do
13
+ { hello: 'world' }
14
+ end
15
+
16
+ get 'hello/:id' do
17
+ { hello: 'world', id: params[:id] }
18
+ end
19
+ end
20
+
21
+ run GrapeExample
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hanami/router'
4
+ require 'multi_json'
5
+
6
+ app = Hanami::Router.new do
7
+ get '/hello', to: ->(_env) { [200, {}, [MultiJson.dump(hello: 'world')]] }
8
+ get '/hello/:id', to: lambda { |env|
9
+ [200, {}, [MultiJson.dump(hello: 'world', id: env['router.params'][:id])]]
10
+ }
11
+ post '/hello', to: ->(_env) { [201, {}, [MultiJson.dump(hello: 'world')]] }
12
+ end
13
+
14
+ run app
@@ -0,0 +1,86 @@
1
+ openapi: 3.0.0
2
+ info:
3
+ title: "API"
4
+ version: "1.0.0"
5
+ contact:
6
+ name: Contact Name
7
+ email: contact@example.com
8
+ url: https://example.com/
9
+ tags:
10
+ - name: Metadata
11
+ description: Metadata related requests
12
+ paths:
13
+ /hello/{id}:
14
+ parameters:
15
+ - name: id
16
+ description: ID of the thing to get
17
+ in: path
18
+ required: true
19
+ schema:
20
+ type: string
21
+ get:
22
+ operationId: find_thing
23
+ description: Get one thing
24
+ tags: ["Metadata"]
25
+ responses:
26
+ "200":
27
+ description: OK
28
+ content:
29
+ application/json:
30
+ schema:
31
+ type: array
32
+ items:
33
+ type: object
34
+ required: [hello, id]
35
+ properties:
36
+ hello:
37
+ type: string
38
+ id:
39
+ type: string
40
+ /hello:
41
+ get:
42
+ operationId: find_things
43
+ description: Get multiple things
44
+ tags: ["Metadata"]
45
+ parameters:
46
+ - name: filter
47
+ description: filter things
48
+ in: query
49
+ required: false
50
+ schema:
51
+ type: object
52
+ required: [id]
53
+ properties:
54
+ id:
55
+ type: string
56
+ description: Comma separated list of thing-IDs
57
+
58
+ responses:
59
+ "200":
60
+ description: OK
61
+ content:
62
+ application/json:
63
+ schema:
64
+ type: object
65
+ required: [hello]
66
+ properties:
67
+ hello:
68
+ type: string
69
+ default:
70
+ description: Error response
71
+
72
+ post:
73
+ operationId: create_thing
74
+ description: Create a thing
75
+ tags: ["Metadata"]
76
+ responses:
77
+ "201":
78
+ description: OK
79
+ content:
80
+ application/json:
81
+ schema:
82
+ type: object
83
+ required: [hello]
84
+ properties:
85
+ hello:
86
+ type: string
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'multi_json'
4
+ require 'openapi_first'
5
+
6
+ namespace = Module.new do
7
+ def self.find_thing(params, _res)
8
+ { hello: 'world', id: params.fetch('id') }
9
+ end
10
+
11
+ def self.find_things(_params, _res)
12
+ [{ hello: 'world' }]
13
+ end
14
+
15
+ def self.create_thing(_params, res)
16
+ res.status = 201
17
+ { hello: 'world' }
18
+ end
19
+ end
20
+
21
+ oas_path = File.absolute_path('./openapi.yaml', __dir__)
22
+ run OpenapiFirst.app(oas_path, namespace: namespace)
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openapi_first'
4
+ require 'syro'
5
+ require 'multi_json'
6
+
7
+ app = Syro.new do
8
+ on 'hello' do
9
+ on :id do
10
+ get do
11
+ res.json MultiJson.dump(hello: 'world', id: inbox[:id])
12
+ end
13
+ end
14
+
15
+ get do
16
+ res.json [MultiJson.dump(hello: 'world')]
17
+ end
18
+
19
+ post do
20
+ res.status = 201
21
+ res.json MultiJson.dump(hello: 'world')
22
+ end
23
+ end
24
+ end
25
+
26
+ spec = OpenapiFirst.load(File.absolute_path('./openapi.yaml', __dir__))
27
+ use OpenapiFirst::Router, spec: spec
28
+ use OpenapiFirst::QueryParameterValidation
29
+ use OpenapiFirst::RequestBodyValidation
30
+
31
+ run app
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'multi_json'
4
+ require 'openapi_first'
5
+
6
+ namespace = Module.new do
7
+ def self.find_thing(params, _res)
8
+ { hello: 'world', id: params.fetch('id') }
9
+ end
10
+
11
+ def self.find_things(_params, _res)
12
+ [{ hello: 'world' }]
13
+ end
14
+
15
+ def self.create_thing(_params, res)
16
+ res.status = 201
17
+ { hello: 'world' }
18
+ end
19
+ end
20
+
21
+ spec = OpenapiFirst.load(File.absolute_path('./openapi.yaml', __dir__))
22
+ use OpenapiFirst::Router, spec: spec
23
+ run OpenapiFirst::OperationResolver.new(namespace: namespace)
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'multi_json'
4
+ require 'sinatra/base'
5
+
6
+ class SinatraExample < Sinatra::Base
7
+ set :environment, :production
8
+
9
+ get '/hello/:id' do
10
+ content_type :json
11
+ MultiJson.dump(hello: 'world', id: params.fetch('id'))
12
+ end
13
+
14
+ get '/hello' do
15
+ content_type :json
16
+ [MultiJson.dump(hello: 'world')]
17
+ end
18
+
19
+ post '/hello' do
20
+ content_type :json
21
+ status 201
22
+ MultiJson.dump(hello: 'world')
23
+ end
24
+ end
25
+
26
+ run SinatraExample
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'syro'
4
+ require 'multi_json'
5
+
6
+ app = Syro.new do
7
+ on 'hello' do
8
+ on :id do
9
+ get do
10
+ res.json MultiJson.dump(hello: 'world', id: inbox[:id])
11
+ end
12
+ end
13
+
14
+ get do
15
+ res.json [MultiJson.dump(hello: 'world')]
16
+ end
17
+
18
+ post do
19
+ res.status = 201
20
+ res.json MultiJson.dump(hello: 'world')
21
+ end
22
+ end
23
+ end
24
+
25
+ run app
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark/ips'
4
+ require 'benchmark/memory'
5
+ require 'rack'
6
+ ENV['RACK_ENV'] = 'production'
7
+
8
+ examples = [
9
+ [Rack::MockRequest.env_for('/hello'), 200],
10
+ [Rack::MockRequest.env_for('/unknown'), 404],
11
+ [Rack::MockRequest.env_for('/hello', method: 'POST'), 201],
12
+ [Rack::MockRequest.env_for('/hello/1'), 200],
13
+ [Rack::MockRequest.env_for('/hello/123'), 200],
14
+ [Rack::MockRequest.env_for('/hello?filter[id]=1,2'), 200]
15
+ ]
16
+
17
+ apps = Dir['./apps/*.ru'].each_with_object({}) do |config, hash|
18
+ hash[config] = Rack::Builder.parse_file(config).first
19
+ end
20
+ apps.freeze
21
+
22
+ Benchmark.ips do |x|
23
+ apps.each do |config, app|
24
+ x.report(config) do
25
+ examples.each do |example|
26
+ env, expected_status = example
27
+ response = app.call(env)
28
+ raise unless response[0] == expected_status
29
+ end
30
+ end
31
+ end
32
+ x.compare!
33
+ end
34
+
35
+ Benchmark.memory do |x|
36
+ apps.each do |config, app|
37
+ x.report(config) do
38
+ examples.each do |example|
39
+ env, expected_status = example
40
+ response = app.call(env)
41
+ raise unless response[0] == expected_status
42
+ end
43
+ end
44
+ end
45
+ x.compare!
46
+ end
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'yaml'
3
4
  require 'oas_parser'
4
5
  require 'openapi_first/definition'
5
6
  require 'openapi_first/version'
6
7
  require 'openapi_first/router'
7
8
  require 'openapi_first/query_parameter_validation'
8
9
  require 'openapi_first/request_body_validation'
10
+ require 'openapi_first/response_validator'
9
11
  require 'openapi_first/operation_resolver'
10
12
  require 'openapi_first/app'
11
13
 
@@ -15,8 +17,12 @@ module OpenapiFirst
15
17
  REQUEST_BODY = 'openapi_first.parsed_request_body'
16
18
  QUERY_PARAMS = 'openapi_first.query_params'
17
19
 
18
- def self.load(spec_path)
19
- Definition.new(OasParser::Definition.resolve(spec_path))
20
+ def self.load(spec_path, only: nil)
21
+ content = YAML.load_file(spec_path)
22
+ raw = OasParser::Parser.new(spec_path, content).resolve
23
+ raw['paths'].filter!(&->(key, _) { only.call(key) }) if only
24
+ parsed = OasParser::Definition.new(raw, spec_path)
25
+ Definition.new(parsed)
20
26
  end
21
27
 
22
28
  def self.app(spec, namespace:)
@@ -14,7 +14,7 @@ module OpenapiFirst
14
14
  MultiJson.dump(errors: errors),
15
15
  status,
16
16
  Rack::CONTENT_TYPE => 'application/vnd.api+json'
17
- )
17
+ ).finish
18
18
  end
19
19
  end
20
20
  end
@@ -4,23 +4,40 @@ require 'rack'
4
4
 
5
5
  module OpenapiFirst
6
6
  class OperationResolver
7
- DEFAULT_APP = ->(_env) { Rack::Response.new('', 404) }
7
+ NOT_FOUND = Rack::Response.new('', 404).finish.freeze
8
+ DEFAULT_APP = ->(_env) { NOT_FOUND }
8
9
 
9
10
  def initialize(app = DEFAULT_APP, namespace:)
10
11
  @app = app
11
12
  @namespace = namespace
12
13
  end
13
14
 
14
- def call(env)
15
+ def call(env) # rubocop:disable Metrics/AbcSize
15
16
  operation = env[OpenapiFirst::OPERATION]
16
17
  return @app.call(env) unless operation
17
18
 
18
19
  operation_id = operation.operation_id
19
20
  res = Rack::Response.new
20
- result = call_operation_method(operation_id, env, res)
21
+ params = build_params(env)
22
+ handler = find_handler(operation_id)
23
+ result = handler.call(params, res)
21
24
  res.write MultiJson.dump(result) if result && res.body.empty?
22
25
  res[Rack::CONTENT_TYPE] ||= find_content_type(operation, res.status)
23
- res
26
+ res.finish
27
+ end
28
+
29
+ def find_handler(operation_id)
30
+ if operation_id.include?('.')
31
+ module_name, method_name = operation_id.split('.')
32
+ return @namespace.const_get(module_name.camelize).method(method_name)
33
+ end
34
+
35
+ if operation_id.include?('#')
36
+ module_name, class_name = operation_id.split('#')
37
+ return @namespace.const_get(module_name.camelize)
38
+ .const_get(class_name.camelize).new
39
+ end
40
+ @namespace.method(operation_id)
24
41
  end
25
42
 
26
43
  private
@@ -32,15 +49,6 @@ module OpenapiFirst
32
49
  content.keys[0] if content
33
50
  end
34
51
 
35
- def call_operation_method(operation_id, env, res)
36
- target = @namespace
37
- methods = operation_id.split('.')
38
- final = methods.pop
39
- methods.each { |m| target = target.send(m) }
40
- params = build_params(env)
41
- target.send(final, params, res)
42
- end
43
-
44
52
  def build_params(env)
45
53
  sources = [
46
54
  env[PATH_PARAMS],
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ class QueryParameterSchemas
5
+ def initialize(allow_additional_parameters:)
6
+ @additional_properties = allow_additional_parameters
7
+ end
8
+
9
+ def find(operation)
10
+ build_parameter_schema(operation)
11
+ end
12
+
13
+ private
14
+
15
+ def build_parameter_schema(operation)
16
+ return unless operation&.query_parameters&.any?
17
+
18
+ operation.query_parameters.each_with_object(
19
+ 'type' => 'object',
20
+ 'required' => [],
21
+ 'additionalProperties' => @additional_properties,
22
+ 'properties' => {}
23
+ ) do |parameter, schema|
24
+ schema['required'] << parameter.name if parameter.required
25
+ schema['properties'][parameter.name] = parameter.schema
26
+ end
27
+ end
28
+ end
29
+ end
@@ -5,6 +5,7 @@ require 'json_schemer'
5
5
  require 'multi_json'
6
6
  require_relative 'validation_format'
7
7
  require_relative 'error_response_method'
8
+ require_relative 'query_parameter_schemas'
8
9
 
9
10
  module OpenapiFirst
10
11
  class QueryParameterValidation
@@ -12,14 +13,18 @@ module OpenapiFirst
12
13
 
13
14
  def initialize(app, allow_additional_parameters: false)
14
15
  @app = app
16
+ @schemas = QueryParameterSchemas.new(
17
+ allow_additional_parameters: allow_additional_parameters
18
+ )
15
19
  @additional_properties = allow_additional_parameters
16
20
  end
17
21
 
18
22
  def call(env)
19
23
  req = Rack::Request.new(env)
20
- schema = parameter_schema(env[OpenapiFirst::OPERATION])
21
- params = req.params
24
+ operation = env[OpenapiFirst::OPERATION]
25
+ schema = operation && @schemas.find(operation)
22
26
  if schema
27
+ params = req.params
23
28
  errors = schema && JSONSchemer.schema(schema).validate(params)
24
29
  return error_response(400, serialize_errors(errors)) if errors&.any?
25
30
 
@@ -38,20 +43,6 @@ module OpenapiFirst
38
43
  end
39
44
  end
40
45
 
41
- def parameter_schema(operation)
42
- return unless operation&.query_parameters&.any?
43
-
44
- operation.query_parameters.each_with_object(
45
- 'type' => 'object',
46
- 'required' => [],
47
- 'additionalProperties' => @additional_properties,
48
- 'properties' => {}
49
- ) do |parameter, schema|
50
- schema['required'] << parameter.name if parameter.required
51
- schema['properties'][parameter.name] = parameter.schema
52
- end
53
- end
54
-
55
46
  def serialize_errors(validation_errors)
56
47
  validation_errors.map do |error|
57
48
  {
@@ -7,6 +7,8 @@ require 'mustermann/template'
7
7
 
8
8
  module OpenapiFirst
9
9
  class Router
10
+ NOT_FOUND = Rack::Response.new('', 404).finish.freeze
11
+
10
12
  def initialize(app, spec:, allow_unknown_operation: false)
11
13
  @app = app
12
14
  @spec = spec
@@ -20,7 +22,7 @@ module OpenapiFirst
20
22
  env[PATH_PARAMS] = path_params if path_params
21
23
  return @app.call(env) if operation || @allow_unknown_operation
22
24
 
23
- Rack::Response.new('', 404)
25
+ NOT_FOUND
24
26
  end
25
27
 
26
28
  def find_path_params(operation, req)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.6.1'
4
+ VERSION = '0.6.3'
5
5
  end
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.email = ['andreas.haller@posteo.de']
12
12
  spec.licenses = ['MIT']
13
13
 
14
- spec.summary = 'Implement REST APIs based on an OpenApi API description'
14
+ spec.summary = 'Implement REST APIs based on OpenApi.'
15
15
  spec.homepage = 'https://github.com/ahx/openapi_first'
16
16
 
17
17
  if spec.respond_to?(:metadata)
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.6.1
4
+ version: 0.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-07-10 00:00:00.000000000 Z
11
+ date: 2019-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json_schemer
@@ -139,6 +139,18 @@ files:
139
139
  - Gemfile.lock
140
140
  - README.md
141
141
  - Rakefile
142
+ - benchmarks/Gemfile
143
+ - benchmarks/Gemfile.lock
144
+ - benchmarks/apps/committee.ru
145
+ - benchmarks/apps/grape.ru
146
+ - benchmarks/apps/hanami_router.ru
147
+ - benchmarks/apps/openapi.yaml
148
+ - benchmarks/apps/openapi_first.ru
149
+ - benchmarks/apps/openapi_first_request_validation_only.ru
150
+ - benchmarks/apps/openapi_first_resolve_only.ru
151
+ - benchmarks/apps/sinatra.ru
152
+ - benchmarks/apps/syro.ru
153
+ - benchmarks/benchmarks.rb
142
154
  - bin/console
143
155
  - bin/setup
144
156
  - examples/README.md
@@ -151,6 +163,7 @@ files:
151
163
  - lib/openapi_first/definition.rb
152
164
  - lib/openapi_first/error_response_method.rb
153
165
  - lib/openapi_first/operation_resolver.rb
166
+ - lib/openapi_first/query_parameter_schemas.rb
154
167
  - lib/openapi_first/query_parameter_validation.rb
155
168
  - lib/openapi_first/request_body_validation.rb
156
169
  - lib/openapi_first/response_validator.rb
@@ -182,5 +195,5 @@ requirements: []
182
195
  rubygems_version: 3.0.3
183
196
  signing_key:
184
197
  specification_version: 4
185
- summary: Implement REST APIs based on an OpenApi API description
198
+ summary: Implement REST APIs based on OpenApi.
186
199
  test_files: []