openapi_first 0.6.1 → 0.6.3

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: 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: []