openapi_first 0.6.0
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 +7 -0
- data/.github/CODEOWNERS +1 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +108 -0
- data/README.md +243 -0
- data/Rakefile +15 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/examples/README.md +13 -0
- data/examples/app.rb +12 -0
- data/examples/config.ru +7 -0
- data/examples/openapi.yaml +29 -0
- data/lib/openapi_first.rb +43 -0
- data/lib/openapi_first/app.rb +27 -0
- data/lib/openapi_first/coverage.rb +35 -0
- data/lib/openapi_first/error_response_method.rb +20 -0
- data/lib/openapi_first/operation_resolver.rb +55 -0
- data/lib/openapi_first/query_parameter_validation.rb +65 -0
- data/lib/openapi_first/request_body_validation.rb +92 -0
- data/lib/openapi_first/response_validator.rb +52 -0
- data/lib/openapi_first/router.rb +40 -0
- data/lib/openapi_first/validation.rb +15 -0
- data/lib/openapi_first/validation_format.rb +25 -0
- data/lib/openapi_first/version.rb +5 -0
- data/openapi_first.gemspec +42 -0
- metadata +185 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 55b0bba2e51dd0517e3306e34d345ec34d2165a2a3d8536c140941b74818d945
|
|
4
|
+
data.tar.gz: 8ba6d8d04a3abb38e60a2716f7515df945b870e0b8a874b6d217773894e4642c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d072794a009783f7cb4ea4141ffadfaea699701954f1c994dc6db69faf18e36ed554bdeb979d81d5b31d7abb8a668e17113156e96ccf311b99a0187a7d48dc06
|
|
7
|
+
data.tar.gz: 608a19940d52d6e554c6d53a95ea1942731d2a8df9118a3c91202d7b8220c7eb0899bffccbcd9a3ff6213ad520851ab446909bd6e2b4d17950937017179f264b
|
data/.github/CODEOWNERS
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* @ahx
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
openapi_first (0.6.0)
|
|
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 (5.2.3)
|
|
14
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
15
|
+
i18n (>= 0.7, < 2)
|
|
16
|
+
minitest (~> 5.1)
|
|
17
|
+
tzinfo (~> 1.1)
|
|
18
|
+
addressable (2.6.0)
|
|
19
|
+
public_suffix (>= 2.0.2, < 4.0)
|
|
20
|
+
ast (2.4.0)
|
|
21
|
+
builder (3.2.3)
|
|
22
|
+
coderay (1.1.2)
|
|
23
|
+
concurrent-ruby (1.1.5)
|
|
24
|
+
deep_merge (1.2.1)
|
|
25
|
+
diff-lcs (1.3)
|
|
26
|
+
ecma-re-validator (0.2.0)
|
|
27
|
+
regexp_parser (~> 1.2)
|
|
28
|
+
hana (1.3.5)
|
|
29
|
+
hansi (0.2.0)
|
|
30
|
+
i18n (1.6.0)
|
|
31
|
+
concurrent-ruby (~> 1.0)
|
|
32
|
+
jaro_winkler (1.5.2)
|
|
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)
|
|
38
|
+
method_source (0.9.2)
|
|
39
|
+
mini_portile2 (2.4.0)
|
|
40
|
+
minitest (5.11.3)
|
|
41
|
+
multi_json (1.13.1)
|
|
42
|
+
mustermann (1.0.3)
|
|
43
|
+
mustermann-contrib (1.0.3)
|
|
44
|
+
hansi (~> 0.2.0)
|
|
45
|
+
mustermann (= 1.0.3)
|
|
46
|
+
nokogiri (1.10.3)
|
|
47
|
+
mini_portile2 (~> 2.4.0)
|
|
48
|
+
oas_parser (0.19.0)
|
|
49
|
+
activesupport (>= 4.0.0)
|
|
50
|
+
addressable (~> 2.3)
|
|
51
|
+
builder (~> 3.2.3)
|
|
52
|
+
deep_merge (~> 1.2.1)
|
|
53
|
+
mustermann-contrib (~> 1.0.3s)
|
|
54
|
+
nokogiri
|
|
55
|
+
parallel (1.17.0)
|
|
56
|
+
parser (2.6.3.0)
|
|
57
|
+
ast (~> 2.4.0)
|
|
58
|
+
pry (0.12.2)
|
|
59
|
+
coderay (~> 1.1.0)
|
|
60
|
+
method_source (~> 0.9.0)
|
|
61
|
+
public_suffix (3.1.1)
|
|
62
|
+
rack (2.0.7)
|
|
63
|
+
rack-test (1.1.0)
|
|
64
|
+
rack (>= 1.0, < 3)
|
|
65
|
+
rainbow (3.0.0)
|
|
66
|
+
rake (10.5.0)
|
|
67
|
+
regexp_parser (1.2.0)
|
|
68
|
+
rspec (3.8.0)
|
|
69
|
+
rspec-core (~> 3.8.0)
|
|
70
|
+
rspec-expectations (~> 3.8.0)
|
|
71
|
+
rspec-mocks (~> 3.8.0)
|
|
72
|
+
rspec-core (3.8.0)
|
|
73
|
+
rspec-support (~> 3.8.0)
|
|
74
|
+
rspec-expectations (3.8.3)
|
|
75
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
76
|
+
rspec-support (~> 3.8.0)
|
|
77
|
+
rspec-mocks (3.8.0)
|
|
78
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
79
|
+
rspec-support (~> 3.8.0)
|
|
80
|
+
rspec-support (3.8.0)
|
|
81
|
+
rubocop (0.69.0)
|
|
82
|
+
jaro_winkler (~> 1.5.1)
|
|
83
|
+
parallel (~> 1.10)
|
|
84
|
+
parser (>= 2.6)
|
|
85
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
86
|
+
ruby-progressbar (~> 1.7)
|
|
87
|
+
unicode-display_width (>= 1.4.0, < 1.7)
|
|
88
|
+
ruby-progressbar (1.10.0)
|
|
89
|
+
thread_safe (0.3.6)
|
|
90
|
+
tzinfo (1.2.5)
|
|
91
|
+
thread_safe (~> 0.1)
|
|
92
|
+
unicode-display_width (1.6.0)
|
|
93
|
+
uri_template (0.7.0)
|
|
94
|
+
|
|
95
|
+
PLATFORMS
|
|
96
|
+
ruby
|
|
97
|
+
|
|
98
|
+
DEPENDENCIES
|
|
99
|
+
bundler (~> 2.0)
|
|
100
|
+
openapi_first!
|
|
101
|
+
pry
|
|
102
|
+
rack-test (~> 1)
|
|
103
|
+
rake (~> 10.0)
|
|
104
|
+
rspec (~> 3.0)
|
|
105
|
+
rubocop
|
|
106
|
+
|
|
107
|
+
BUNDLED WITH
|
|
108
|
+
2.0.1
|
data/README.md
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# OpenapiFirst
|
|
2
|
+
|
|
3
|
+
OpenapiFirst helps to implement Rack based 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.
|
|
4
|
+
|
|
5
|
+
## TL;DR
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
module Pets
|
|
9
|
+
def self.find_pet(params, _res) # "find_pet" is an operationId from your OpenApi file
|
|
10
|
+
{
|
|
11
|
+
id: params['id'],
|
|
12
|
+
name: 'Oscar'
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# In config.ru:
|
|
18
|
+
require 'openapi_first'
|
|
19
|
+
run OpenapiFirst.app('./openapi/openapi.yaml', namespace: Pets)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The above will:
|
|
23
|
+
|
|
24
|
+
- Validate the request and respond with 400 if the request does not match against your spec
|
|
25
|
+
- Map the request (for example `GET /pet/1`) to the method call `Pets.find_pet`
|
|
26
|
+
- Set the response content type according to your spec (here with the default status code `200`)
|
|
27
|
+
|
|
28
|
+
### Usage as Rack middlware
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# Just like the above, except the last line
|
|
32
|
+
# ...
|
|
33
|
+
run OpenapiFirst.middleware('./openapi/openapi.yaml', namespace: Pets)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
When using the middleware, all requests that are not part of the API description will be passed to the next app.
|
|
37
|
+
|
|
38
|
+
## Try it out
|
|
39
|
+
|
|
40
|
+
See [example](examples)
|
|
41
|
+
|
|
42
|
+
## Missing features
|
|
43
|
+
See [issues](https://github.com/ahx/openapi_first/issues).
|
|
44
|
+
|
|
45
|
+
## Start
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
We recommend saving the file as `openapi/openapi.yaml`.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
Add this line to your application's Gemfile:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
gem 'openapi_first'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
|
|
60
|
+
|
|
61
|
+
## How it works
|
|
62
|
+
|
|
63
|
+
OpenapiFirst offers Rack middlewares to auto-implement different aspects of request validation:
|
|
64
|
+
|
|
65
|
+
- Query parameter validation
|
|
66
|
+
- Request body validation
|
|
67
|
+
- Mapping request to a function call
|
|
68
|
+
|
|
69
|
+
It starts with a router middleware:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
spec = OpenapiFirst.load('petstore.yaml')
|
|
73
|
+
use OpenapiFirst::Router, spec: spec
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If the request is not valid, these middlewares return a 400 status code with a body that describes the error. If unkwon routes in your application exist, which are not specified in the openapi spec file, set `:allow_unknown_operation` to `true`.
|
|
77
|
+
|
|
78
|
+
The error responses conform with [JSON:API](https://jsonapi.org).
|
|
79
|
+
|
|
80
|
+
Here's an example response body for a missing query parameter "search":
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
http-status: 400
|
|
84
|
+
content-type: "application/vnd.api+json"
|
|
85
|
+
|
|
86
|
+
{
|
|
87
|
+
"errors": [
|
|
88
|
+
{
|
|
89
|
+
"title": "is missing",
|
|
90
|
+
"source": {
|
|
91
|
+
"parameter": "search"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Query parameter validation
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
use OpenapiFirst::QueryParameterValidation
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
By default OpenapiFirst does not allow additional query parameters and will respond with 400 if additional parameters are sent. You can allow additional parameters with `additional_properties: true`:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
use OpenapiFirst::QueryParameterValidation,
|
|
108
|
+
allow_additional_parameters: true
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The middleware filteres all top-level query parameters and adds these to the Rack env: `env[OpenapiFirst::QUERY_PARAMS]`.
|
|
112
|
+
If you want to forbid nested query parameters you will need to use `additionalProperties: false` in your query parameter json schema.
|
|
113
|
+
|
|
114
|
+
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).
|
|
115
|
+
|
|
116
|
+
### TODO: Header, Cookie, Path parameter validation
|
|
117
|
+
|
|
118
|
+
tbd.
|
|
119
|
+
|
|
120
|
+
### Request Body validation
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
# Add the middleware:
|
|
124
|
+
use OpenapiFirst::RequestBodyValidation
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
This will return a `415` if the requests content type does not match or `400` if the request body is invalid.
|
|
128
|
+
This will add the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
|
|
129
|
+
|
|
130
|
+
OpenAPI request (and response) body validation is based on [JSON Schema](http://json-schema.org/).
|
|
131
|
+
|
|
132
|
+
### Mapping request to a function call
|
|
133
|
+
|
|
134
|
+
OpenapiFirst has a `OperationResolver` middleware to map the HTTP request to a function (method) call
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
# Define some methods
|
|
138
|
+
module MyApi
|
|
139
|
+
def create_pet(params, res)
|
|
140
|
+
res.status = 201
|
|
141
|
+
{
|
|
142
|
+
id: '1',
|
|
143
|
+
name: params['name']
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Add the middleware:
|
|
149
|
+
use OpenapiFirst::OperationResolver, namespace: MyApi
|
|
150
|
+
# If the operation was not found in the OAS file, the next app will be called
|
|
151
|
+
|
|
152
|
+
# OR use it as a Rack app via `run`:
|
|
153
|
+
run OpenapiFirst::OperationResolver, namespace: Pets
|
|
154
|
+
# If the operation was not found, this will return 404
|
|
155
|
+
|
|
156
|
+
# Now make a request like
|
|
157
|
+
# POST /pets, { name: 'Oscar' }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
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)`.
|
|
161
|
+
|
|
162
|
+
These resolver functions are called with two arguments:
|
|
163
|
+
|
|
164
|
+
- `params` - Holds the parsed request body, filtered query params and path parameters
|
|
165
|
+
- `res` - Holds a Rack::Response that you can modify if needed
|
|
166
|
+
|
|
167
|
+
You can call `params.env` to access the Rack env (just like in [Hanami actions](https://guides.hanamirb.org/actions/parameters/))
|
|
168
|
+
|
|
169
|
+
There are two ways to set the response body:
|
|
170
|
+
|
|
171
|
+
- Calling `res.write "things"` (see [Rack::Response](https://www.rubydoc.info/github/rack/rack/Rack/Response))
|
|
172
|
+
- Returning a value from the function (see example above) (this will always converted to JSON)
|
|
173
|
+
|
|
174
|
+
## Testing
|
|
175
|
+
|
|
176
|
+
OpenapiFirst offers tools to help testing your app.
|
|
177
|
+
|
|
178
|
+
### Response validation
|
|
179
|
+
|
|
180
|
+
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).
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
# In your test:
|
|
184
|
+
require 'openapi_first/response_validator'
|
|
185
|
+
spec = OpenapiFirst.load('petstore.yaml')
|
|
186
|
+
validator = OpenapiFirst::ResponseValidator.new(spec)
|
|
187
|
+
validator.validate(last_request, last_response).errors? # => true or false
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
TODO: Add RSpec matcher (via extra rubygem)
|
|
191
|
+
|
|
192
|
+
### Coverage
|
|
193
|
+
|
|
194
|
+
(This is a bit experimental. Please try it out and give feedback.)
|
|
195
|
+
|
|
196
|
+
`OpenapiFirst::Coverage` helps you make sure, that you have called all endpoints of your OAS file when running tests via `rack-test`.
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
# In your test (rspec example):
|
|
200
|
+
require 'openapi_first/coverage'
|
|
201
|
+
|
|
202
|
+
describe MyApp do
|
|
203
|
+
include Rack::Test::Methods
|
|
204
|
+
|
|
205
|
+
before(:all) do
|
|
206
|
+
spec = OpenapiFirst.load('petstore.yaml')
|
|
207
|
+
@app_wrapper = OpenapiFirst::Coverage.new(MyApp, spec)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
after(:all) do
|
|
211
|
+
message = "The following paths have not been called yet: #{@app_wrapper.to_be_called}"
|
|
212
|
+
expect(@app_wrapper.to_be_called).to be_empty
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Overwrite `#app` to make rack-test call the wrapped app
|
|
216
|
+
def app
|
|
217
|
+
@app_wrapper
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'does things' do
|
|
221
|
+
get '/i/my/stuff'
|
|
222
|
+
# …
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Mocking
|
|
228
|
+
|
|
229
|
+
Currently out of scope. Use https://github.com/JustinFeng/fakeit or something else.
|
|
230
|
+
|
|
231
|
+
## Alternatives
|
|
232
|
+
|
|
233
|
+
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.
|
|
234
|
+
|
|
235
|
+
## Development
|
|
236
|
+
|
|
237
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
238
|
+
|
|
239
|
+
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).
|
|
240
|
+
|
|
241
|
+
## Contributing
|
|
242
|
+
|
|
243
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ahx/openapi_first.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
|
+
require 'rubocop/rake_task'
|
|
6
|
+
|
|
7
|
+
RuboCop::RakeTask.new
|
|
8
|
+
|
|
9
|
+
task :version do
|
|
10
|
+
puts Gem::Specification.load('openapi_first.gemspec').version
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
14
|
+
|
|
15
|
+
task default: %i[spec rubocop]
|
data/bin/console
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'bundler/setup'
|
|
5
|
+
require 'openapi_first'
|
|
6
|
+
|
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
9
|
+
|
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
11
|
+
# require "pry"
|
|
12
|
+
# Pry.start
|
|
13
|
+
|
|
14
|
+
require 'irb'
|
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/examples/README.md
ADDED
data/examples/app.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openapi_first'
|
|
4
|
+
|
|
5
|
+
module Example
|
|
6
|
+
def self.find_thing(_params, _res)
|
|
7
|
+
{ hello: 'world' }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
oas_path = File.absolute_path('./openapi.yaml', __dir__)
|
|
12
|
+
App = OpenapiFirst.app(oas_path, namespace: Example)
|
data/examples/config.ru
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
/:
|
|
14
|
+
get:
|
|
15
|
+
operationId: find_thing
|
|
16
|
+
summary: Get metadata from the root of the API
|
|
17
|
+
tags: ["Metadata"]
|
|
18
|
+
responses:
|
|
19
|
+
"200":
|
|
20
|
+
description: OK
|
|
21
|
+
content:
|
|
22
|
+
application/json:
|
|
23
|
+
schema:
|
|
24
|
+
type: object
|
|
25
|
+
required: [hello]
|
|
26
|
+
properties:
|
|
27
|
+
hello:
|
|
28
|
+
type: string
|
|
29
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'oas_parser'
|
|
4
|
+
require 'openapi_first/version'
|
|
5
|
+
require 'openapi_first/router'
|
|
6
|
+
require 'openapi_first/query_parameter_validation'
|
|
7
|
+
require 'openapi_first/request_body_validation'
|
|
8
|
+
require 'openapi_first/operation_resolver'
|
|
9
|
+
require 'openapi_first/app'
|
|
10
|
+
|
|
11
|
+
module OpenapiFirst
|
|
12
|
+
OPERATION = 'openapi_first.operation'
|
|
13
|
+
PATH_PARAMS = 'openapi_first.path_params'
|
|
14
|
+
REQUEST_BODY = 'openapi_first.parsed_request_body'
|
|
15
|
+
QUERY_PARAMS = 'openapi_first.query_params'
|
|
16
|
+
|
|
17
|
+
def self.load(spec_path)
|
|
18
|
+
OasParser::Definition.resolve(spec_path)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.app(spec, namespace:)
|
|
22
|
+
spec = OpenapiFirst.load(spec) if spec.is_a?(String)
|
|
23
|
+
App.new(spec, namespace: namespace)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.middleware(spec, namespace:)
|
|
27
|
+
spec = OpenapiFirst.load(spec) if spec.is_a?(String)
|
|
28
|
+
AppWithOptions.new(spec, namespace: namespace)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class AppWithOptions
|
|
32
|
+
def initialize(*options)
|
|
33
|
+
@options = options
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def new(app)
|
|
37
|
+
App.new(app, *@options)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class Error < StandardError; end
|
|
42
|
+
# Your code goes here...
|
|
43
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack'
|
|
4
|
+
|
|
5
|
+
module OpenapiFirst
|
|
6
|
+
class App
|
|
7
|
+
def initialize(
|
|
8
|
+
app = nil, # rubocop:disable Style/OptionalArguments
|
|
9
|
+
spec,
|
|
10
|
+
namespace:,
|
|
11
|
+
allow_unknown_operation: !app.nil?
|
|
12
|
+
)
|
|
13
|
+
@stack = Rack::Builder.new do
|
|
14
|
+
use OpenapiFirst::Router,
|
|
15
|
+
spec: spec,
|
|
16
|
+
allow_unknown_operation: allow_unknown_operation
|
|
17
|
+
use OpenapiFirst::QueryParameterValidation
|
|
18
|
+
use OpenapiFirst::RequestBodyValidation
|
|
19
|
+
run OpenapiFirst::OperationResolver.new(app, namespace: namespace)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(env)
|
|
24
|
+
@stack.call(env)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiFirst
|
|
4
|
+
class Coverage
|
|
5
|
+
attr_reader :to_be_called
|
|
6
|
+
|
|
7
|
+
def initialize(app, spec)
|
|
8
|
+
@app = app
|
|
9
|
+
@spec = spec
|
|
10
|
+
@to_be_called = spec.endpoints.map do |endpoint|
|
|
11
|
+
endpoint_id(endpoint)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(env)
|
|
16
|
+
endpoint = endpoint_for_request(Rack::Request.new(env))
|
|
17
|
+
@to_be_called.delete(endpoint_id(endpoint)) if endpoint
|
|
18
|
+
@app.call(env)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def endpoint_id(endpoint)
|
|
24
|
+
"#{endpoint.path.path}##{endpoint.method}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def endpoint_for_request(request)
|
|
28
|
+
@spec
|
|
29
|
+
.path_by_path(request.path)
|
|
30
|
+
.endpoint_by_method(request.request_method.downcase)
|
|
31
|
+
rescue OasParser::PathNotFound
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiFirst
|
|
4
|
+
module ErrorResponseMethod
|
|
5
|
+
def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
|
|
6
|
+
{
|
|
7
|
+
status: status.to_s,
|
|
8
|
+
title: title
|
|
9
|
+
}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def error_response(status, errors = [default_error(status)])
|
|
13
|
+
Rack::Response.new(
|
|
14
|
+
MultiJson.dump(errors: errors),
|
|
15
|
+
status,
|
|
16
|
+
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack'
|
|
4
|
+
|
|
5
|
+
module OpenapiFirst
|
|
6
|
+
class OperationResolver
|
|
7
|
+
DEFAULT_APP = ->(_env) { Rack::Response.new('', 404) }
|
|
8
|
+
|
|
9
|
+
def initialize(app = DEFAULT_APP, namespace:)
|
|
10
|
+
@app = app
|
|
11
|
+
@namespace = namespace
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(env)
|
|
15
|
+
operation = env[OpenapiFirst::OPERATION]
|
|
16
|
+
return @app.call(env) unless operation
|
|
17
|
+
|
|
18
|
+
operation_id = operation.operation_id
|
|
19
|
+
res = Rack::Response.new
|
|
20
|
+
result = call_operation_method(operation_id, env, res)
|
|
21
|
+
res.write MultiJson.dump(result) if result && res.body.empty?
|
|
22
|
+
res[Rack::CONTENT_TYPE] ||= find_content_type(operation, res.status)
|
|
23
|
+
res
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def find_content_type(operation, status)
|
|
29
|
+
content = operation
|
|
30
|
+
.response_by_code(status.to_s, use_default: true)
|
|
31
|
+
.content
|
|
32
|
+
content.keys[0] if content
|
|
33
|
+
end
|
|
34
|
+
|
|
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
|
+
def build_params(env)
|
|
45
|
+
sources = [
|
|
46
|
+
env[PATH_PARAMS],
|
|
47
|
+
env[QUERY_PARAMS],
|
|
48
|
+
env[REQUEST_BODY]
|
|
49
|
+
].tap(&:compact!)
|
|
50
|
+
hash = {}.merge!(*sources)
|
|
51
|
+
hash.define_singleton_method(:env) { env }
|
|
52
|
+
hash
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack'
|
|
4
|
+
require 'json_schemer'
|
|
5
|
+
require 'multi_json'
|
|
6
|
+
require_relative 'validation_format'
|
|
7
|
+
require_relative 'error_response_method'
|
|
8
|
+
|
|
9
|
+
module OpenapiFirst
|
|
10
|
+
class QueryParameterValidation
|
|
11
|
+
include ErrorResponseMethod
|
|
12
|
+
|
|
13
|
+
def initialize(app, allow_additional_parameters: false)
|
|
14
|
+
@app = app
|
|
15
|
+
@additional_properties = allow_additional_parameters
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(env)
|
|
19
|
+
req = Rack::Request.new(env)
|
|
20
|
+
schema = parameter_schema(env[OpenapiFirst::OPERATION])
|
|
21
|
+
params = req.params
|
|
22
|
+
if schema
|
|
23
|
+
errors = schema && JSONSchemer.schema(schema).validate(params)
|
|
24
|
+
return error_response(400, serialize_errors(errors)) if errors&.any?
|
|
25
|
+
|
|
26
|
+
req.env[QUERY_PARAMS] = allowed_query_parameters(schema, params)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@app.call(env)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def allowed_query_parameters(params_schema, query_params)
|
|
33
|
+
params_schema['properties']
|
|
34
|
+
.keys
|
|
35
|
+
.each_with_object({}) do |parameter_name, filtered|
|
|
36
|
+
value = query_params[parameter_name]
|
|
37
|
+
filtered[parameter_name] = value if value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
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
|
+
def serialize_errors(validation_errors)
|
|
56
|
+
validation_errors.map do |error|
|
|
57
|
+
{
|
|
58
|
+
source: {
|
|
59
|
+
parameter: File.basename(error['data_pointer'])
|
|
60
|
+
}
|
|
61
|
+
}.update(ValidationFormat.error_details(error))
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack'
|
|
4
|
+
require 'json_schemer'
|
|
5
|
+
require 'multi_json'
|
|
6
|
+
require_relative 'error_response_method'
|
|
7
|
+
require_relative 'validation_format'
|
|
8
|
+
|
|
9
|
+
module OpenapiFirst
|
|
10
|
+
class RequestBodyValidation
|
|
11
|
+
include ErrorResponseMethod
|
|
12
|
+
|
|
13
|
+
def initialize(app)
|
|
14
|
+
@app = app
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(env) # rubocop:disable Metrics/MethodLength
|
|
18
|
+
operation = env[OpenapiFirst::OPERATION]
|
|
19
|
+
return @app.call(env) unless operation&.request_body
|
|
20
|
+
|
|
21
|
+
req = Rack::Request.new(env)
|
|
22
|
+
content_type = req.content_type
|
|
23
|
+
body = req.body
|
|
24
|
+
catch(:halt) do
|
|
25
|
+
validate_request_content_type!(content_type, operation)
|
|
26
|
+
validate_request_body_presence!(env, body, operation)
|
|
27
|
+
parse_and_validate_request_body!(env, content_type, body, operation)
|
|
28
|
+
@app.call(env)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def halt(response)
|
|
33
|
+
throw :halt, response
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def validate_request_content_type!(content_type, operation)
|
|
37
|
+
return if content_type_valid?(content_type, operation)
|
|
38
|
+
|
|
39
|
+
halt(error_response(415))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def validate_request_body_presence!(env, body, operation)
|
|
43
|
+
return unless body.size.zero?
|
|
44
|
+
|
|
45
|
+
if operation.request_body.required
|
|
46
|
+
halt(error_response(415, 'Request body is required'))
|
|
47
|
+
end
|
|
48
|
+
halt(@app.call(env))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_and_validate_request_body!(env, content_type, body, operation)
|
|
52
|
+
schema = request_body_schema(content_type, operation)
|
|
53
|
+
return unless schema
|
|
54
|
+
|
|
55
|
+
parsed_request_body = MultiJson.load(body)
|
|
56
|
+
errors = validate_json_schema(schema, parsed_request_body)
|
|
57
|
+
halt(error_response(400, serialize_errors(errors))) if errors&.any?
|
|
58
|
+
env[OpenapiFirst::REQUEST_BODY] = parsed_request_body
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def validate_json_schema(schema, object)
|
|
62
|
+
JSONSchemer.schema(schema).validate(object)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
|
|
66
|
+
{
|
|
67
|
+
status: status.to_s,
|
|
68
|
+
title: title
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def content_type_valid?(content_type, endpoint)
|
|
73
|
+
endpoint.request_body.content[content_type]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def request_body_schema(content_type, endpoint)
|
|
77
|
+
return unless endpoint
|
|
78
|
+
|
|
79
|
+
endpoint.request_body.content[content_type]&.fetch('schema')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def serialize_errors(validation_errors)
|
|
83
|
+
validation_errors.map do |error|
|
|
84
|
+
{
|
|
85
|
+
source: {
|
|
86
|
+
pointer: error['data_pointer']
|
|
87
|
+
}
|
|
88
|
+
}.update(ValidationFormat.error_details(error))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json_schemer'
|
|
4
|
+
require 'multi_json'
|
|
5
|
+
require_relative 'validation'
|
|
6
|
+
|
|
7
|
+
module OpenapiFirst
|
|
8
|
+
class ResponseValidator
|
|
9
|
+
def initialize(schema)
|
|
10
|
+
@schema = schema
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def validate(request, response)
|
|
14
|
+
errors = validation_errors(request, response)
|
|
15
|
+
Validation.new(errors || [])
|
|
16
|
+
rescue OasParser::ResponseCodeNotFound, OasParser::MethodNotFound => e
|
|
17
|
+
Validation.new([e.message])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def validation_errors(request, response)
|
|
23
|
+
content = response_for(request, response).content
|
|
24
|
+
return unless content
|
|
25
|
+
|
|
26
|
+
content_type = content[response.content_type]
|
|
27
|
+
unless content_type
|
|
28
|
+
return ["Content type not found: '#{response.content_type}'"]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
response_schema = content_type['schema']
|
|
32
|
+
return unless response_schema
|
|
33
|
+
|
|
34
|
+
response_data = MultiJson.load(response.body)
|
|
35
|
+
validate_json_schema(response_schema, response_data)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def validate_json_schema(schema, data)
|
|
39
|
+
JSONSchemer.schema(schema).validate(data).to_a.map do |error|
|
|
40
|
+
error.delete('root_schema')
|
|
41
|
+
error
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def response_for(request, response)
|
|
46
|
+
@schema
|
|
47
|
+
.path_by_path(request.path)
|
|
48
|
+
.endpoint_by_method(request.request_method.downcase)
|
|
49
|
+
.response_by_code(response.status.to_s, use_default: true)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack'
|
|
4
|
+
require 'json_schemer'
|
|
5
|
+
require 'multi_json'
|
|
6
|
+
require 'mustermann/template'
|
|
7
|
+
|
|
8
|
+
module OpenapiFirst
|
|
9
|
+
class Router
|
|
10
|
+
def initialize(app, spec:, allow_unknown_operation: false)
|
|
11
|
+
@app = app
|
|
12
|
+
@spec = spec
|
|
13
|
+
@allow_unknown_operation = allow_unknown_operation
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(env)
|
|
17
|
+
req = Rack::Request.new(env)
|
|
18
|
+
operation = env[OPERATION] = find_operation(req)
|
|
19
|
+
path_params = find_path_params(operation, req)
|
|
20
|
+
env[PATH_PARAMS] = path_params if path_params
|
|
21
|
+
return @app.call(env) if operation || @allow_unknown_operation
|
|
22
|
+
|
|
23
|
+
Rack::Response.new('', 404)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def find_operation(req)
|
|
27
|
+
path = @spec.path_by_path(req.path)
|
|
28
|
+
path.endpoint_by_method(req.request_method.downcase)
|
|
29
|
+
rescue OasParser::PathNotFound, OasParser::MethodNotFound
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def find_path_params(operation, req)
|
|
34
|
+
return unless operation&.path_parameters&.any?
|
|
35
|
+
|
|
36
|
+
pattern = Mustermann::Template.new(operation.path.path)
|
|
37
|
+
pattern.params(req.path)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiFirst
|
|
4
|
+
module ValidationFormat
|
|
5
|
+
# rubocop:disable Metrics/MethodLength
|
|
6
|
+
def self.error_details(error)
|
|
7
|
+
if error['type'] == 'pattern'
|
|
8
|
+
{
|
|
9
|
+
title: 'is not valid',
|
|
10
|
+
detail: "does not match pattern '#{error['schema']['pattern']}'"
|
|
11
|
+
}
|
|
12
|
+
elsif error['type'] == 'required'
|
|
13
|
+
missing_keys = error['details']['missing_keys']
|
|
14
|
+
{
|
|
15
|
+
title: "is missing required properties: #{missing_keys.join(', ')}"
|
|
16
|
+
}
|
|
17
|
+
elsif error['schema'] == false
|
|
18
|
+
{ title: 'unknown fields are not allowed' }
|
|
19
|
+
else
|
|
20
|
+
{ title: 'is not valid' }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
# rubocop:enable Metrics/MethodLength
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
+
require 'openapi_first/version'
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |spec|
|
|
8
|
+
spec.name = 'openapi_first'
|
|
9
|
+
spec.version = OpenapiFirst::VERSION
|
|
10
|
+
spec.authors = ['Andreas Haller']
|
|
11
|
+
spec.email = ['andreas.haller@posteo.de']
|
|
12
|
+
spec.licenses = ['MIT']
|
|
13
|
+
|
|
14
|
+
spec.summary = 'Implement REST APIs based on an OpenApi API description'
|
|
15
|
+
spec.homepage = 'https://github.com/ahx/openapi_first'
|
|
16
|
+
|
|
17
|
+
if spec.respond_to?(:metadata)
|
|
18
|
+
spec.metadata['https://github.com/ahx/openapi_first'] = spec.homepage
|
|
19
|
+
else
|
|
20
|
+
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
|
21
|
+
'public gem pushes.'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
25
|
+
`git ls-files -z`
|
|
26
|
+
.split("\x0")
|
|
27
|
+
.reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
28
|
+
.reject { |f| %w[Dockerfile Jenkinsfile].include?(f) }
|
|
29
|
+
end
|
|
30
|
+
spec.bindir = 'exe'
|
|
31
|
+
spec.require_paths = ['lib']
|
|
32
|
+
|
|
33
|
+
spec.add_dependency 'json_schemer', '~> 0.2'
|
|
34
|
+
spec.add_dependency 'multi_json', '~> 1.13'
|
|
35
|
+
spec.add_dependency 'oas_parser', '~> 0.19'
|
|
36
|
+
spec.add_dependency 'rack', '~> 2'
|
|
37
|
+
|
|
38
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
|
39
|
+
spec.add_development_dependency 'rack-test', '~> 1'
|
|
40
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
|
41
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
42
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: openapi_first
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.6.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andreas Haller
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2019-07-07 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: json_schemer
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.2'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: multi_json
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.13'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.13'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: oas_parser
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0.19'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0.19'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rack
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '2'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '2'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: bundler
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '2.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '2.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rack-test
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '1'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '1'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rake
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '10.0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '10.0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: rspec
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '3.0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '3.0'
|
|
125
|
+
description:
|
|
126
|
+
email:
|
|
127
|
+
- andreas.haller@posteo.de
|
|
128
|
+
executables: []
|
|
129
|
+
extensions: []
|
|
130
|
+
extra_rdoc_files: []
|
|
131
|
+
files:
|
|
132
|
+
- ".github/CODEOWNERS"
|
|
133
|
+
- ".gitignore"
|
|
134
|
+
- ".rspec"
|
|
135
|
+
- ".rubocop.yml"
|
|
136
|
+
- ".travis.yml"
|
|
137
|
+
- CHANGELOG.md
|
|
138
|
+
- Gemfile
|
|
139
|
+
- Gemfile.lock
|
|
140
|
+
- README.md
|
|
141
|
+
- Rakefile
|
|
142
|
+
- bin/console
|
|
143
|
+
- bin/setup
|
|
144
|
+
- examples/README.md
|
|
145
|
+
- examples/app.rb
|
|
146
|
+
- examples/config.ru
|
|
147
|
+
- examples/openapi.yaml
|
|
148
|
+
- lib/openapi_first.rb
|
|
149
|
+
- lib/openapi_first/app.rb
|
|
150
|
+
- lib/openapi_first/coverage.rb
|
|
151
|
+
- lib/openapi_first/error_response_method.rb
|
|
152
|
+
- lib/openapi_first/operation_resolver.rb
|
|
153
|
+
- lib/openapi_first/query_parameter_validation.rb
|
|
154
|
+
- lib/openapi_first/request_body_validation.rb
|
|
155
|
+
- lib/openapi_first/response_validator.rb
|
|
156
|
+
- lib/openapi_first/router.rb
|
|
157
|
+
- lib/openapi_first/validation.rb
|
|
158
|
+
- lib/openapi_first/validation_format.rb
|
|
159
|
+
- lib/openapi_first/version.rb
|
|
160
|
+
- openapi_first.gemspec
|
|
161
|
+
homepage: https://github.com/ahx/openapi_first
|
|
162
|
+
licenses:
|
|
163
|
+
- MIT
|
|
164
|
+
metadata:
|
|
165
|
+
https://github.com/ahx/openapi_first: https://github.com/ahx/openapi_first
|
|
166
|
+
post_install_message:
|
|
167
|
+
rdoc_options: []
|
|
168
|
+
require_paths:
|
|
169
|
+
- lib
|
|
170
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
171
|
+
requirements:
|
|
172
|
+
- - ">="
|
|
173
|
+
- !ruby/object:Gem::Version
|
|
174
|
+
version: '0'
|
|
175
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
176
|
+
requirements:
|
|
177
|
+
- - ">="
|
|
178
|
+
- !ruby/object:Gem::Version
|
|
179
|
+
version: '0'
|
|
180
|
+
requirements: []
|
|
181
|
+
rubygems_version: 3.0.3
|
|
182
|
+
signing_key:
|
|
183
|
+
specification_version: 4
|
|
184
|
+
summary: Implement REST APIs based on an OpenApi API description
|
|
185
|
+
test_files: []
|