openapi_first 0.6.0 → 0.6.1
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 +4 -4
- data/CHANGELOG.md +4 -0
- data/Gemfile.lock +8 -8
- data/README.md +76 -7
- data/lib/openapi_first.rb +2 -1
- data/lib/openapi_first/coverage.rb +6 -14
- data/lib/openapi_first/definition.rb +25 -0
- data/lib/openapi_first/response_validator.rb +16 -9
- data/lib/openapi_first/router.rb +1 -8
- data/lib/openapi_first/validation_format.rb +6 -0
- data/lib/openapi_first/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6888d00f37a7dce1f2b22ad9da04f4ecb69236ea18221173fc77dac1b5edba6c
|
4
|
+
data.tar.gz: 8eca9351e46942b1acf6fa47d3394dbfa9a85c4f0dc3dc1f77c075d2818bc3ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 967dd87e43808f328a5ab72e99cf04c4b37eb756b2a4ef06925e3e419b038583e014cf3c11074168f02fd07b16db4ba266f743a884a608f37e6c1bfeed0ce911
|
7
|
+
data.tar.gz: 9c2a62d290c09417c0c75eaec88427b847f43d787e3cb548dcef543749a98f7252d5915de48fe64c9eb34eca8f579590f1d94bec9455c1ba08932b1a5de5c7f4
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
openapi_first (0.6.
|
4
|
+
openapi_first (0.6.1)
|
5
5
|
json_schemer (~> 0.2)
|
6
6
|
multi_json (~> 1.13)
|
7
7
|
oas_parser (~> 0.19)
|
@@ -29,7 +29,7 @@ GEM
|
|
29
29
|
hansi (0.2.0)
|
30
30
|
i18n (1.6.0)
|
31
31
|
concurrent-ruby (~> 1.0)
|
32
|
-
jaro_winkler (1.5.
|
32
|
+
jaro_winkler (1.5.3)
|
33
33
|
json_schemer (0.2.0)
|
34
34
|
ecma-re-validator (~> 0.2.0)
|
35
35
|
hana (~> 1.3.3)
|
@@ -69,23 +69,23 @@ GEM
|
|
69
69
|
rspec-core (~> 3.8.0)
|
70
70
|
rspec-expectations (~> 3.8.0)
|
71
71
|
rspec-mocks (~> 3.8.0)
|
72
|
-
rspec-core (3.8.
|
72
|
+
rspec-core (3.8.2)
|
73
73
|
rspec-support (~> 3.8.0)
|
74
|
-
rspec-expectations (3.8.
|
74
|
+
rspec-expectations (3.8.4)
|
75
75
|
diff-lcs (>= 1.2.0, < 2.0)
|
76
76
|
rspec-support (~> 3.8.0)
|
77
|
-
rspec-mocks (3.8.
|
77
|
+
rspec-mocks (3.8.1)
|
78
78
|
diff-lcs (>= 1.2.0, < 2.0)
|
79
79
|
rspec-support (~> 3.8.0)
|
80
|
-
rspec-support (3.8.
|
81
|
-
rubocop (0.
|
80
|
+
rspec-support (3.8.2)
|
81
|
+
rubocop (0.72.0)
|
82
82
|
jaro_winkler (~> 1.5.1)
|
83
83
|
parallel (~> 1.10)
|
84
84
|
parser (>= 2.6)
|
85
85
|
rainbow (>= 2.2.2, < 4.0)
|
86
86
|
ruby-progressbar (~> 1.7)
|
87
87
|
unicode-display_width (>= 1.4.0, < 1.7)
|
88
|
-
ruby-progressbar (1.10.
|
88
|
+
ruby-progressbar (1.10.1)
|
89
89
|
thread_safe (0.3.6)
|
90
90
|
tzinfo (1.2.5)
|
91
91
|
thread_safe (~> 0.1)
|
data/README.md
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# OpenapiFirst
|
2
2
|
|
3
|
-
OpenapiFirst helps to implement
|
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.
|
4
4
|
|
5
5
|
## TL;DR
|
6
6
|
|
7
7
|
```ruby
|
8
8
|
module Pets
|
9
|
-
def self.find_pet(params,
|
9
|
+
def self.find_pet(params, res)
|
10
10
|
{
|
11
11
|
id: params['id'],
|
12
12
|
name: 'Oscar'
|
@@ -22,10 +22,16 @@ run OpenapiFirst.app('./openapi/openapi.yaml', namespace: Pets)
|
|
22
22
|
The above will:
|
23
23
|
|
24
24
|
- Validate the request and respond with 400 if the request does not match against your spec
|
25
|
-
- Map the request
|
25
|
+
- Map the request to a method call `Pets.find_pet` based on the `operationId` in the API description
|
26
26
|
- Set the response content type according to your spec (here with the default status code `200`)
|
27
27
|
|
28
|
-
|
28
|
+
Resolver functions (`find_pet`) are called with two arguments:
|
29
|
+
|
30
|
+
- `params` - Holds the parsed request body, filtered query params and path parameters
|
31
|
+
- `res` - Holds a Rack::Response that you can modify if needed
|
32
|
+
If you want to access to plain Rack env you can call `params.env`.
|
33
|
+
|
34
|
+
### Usage as Rack middleware
|
29
35
|
|
30
36
|
```ruby
|
31
37
|
# Just like the above, except the last line
|
@@ -40,6 +46,7 @@ When using the middleware, all requests that are not part of the API description
|
|
40
46
|
See [example](examples)
|
41
47
|
|
42
48
|
## Missing features
|
49
|
+
|
43
50
|
See [issues](https://github.com/ahx/openapi_first/issues).
|
44
51
|
|
45
52
|
## Start
|
@@ -58,6 +65,67 @@ gem 'openapi_first'
|
|
58
65
|
|
59
66
|
OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
|
60
67
|
|
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
|
+
|
61
129
|
## How it works
|
62
130
|
|
63
131
|
OpenapiFirst offers Rack middlewares to auto-implement different aspects of request validation:
|
@@ -73,7 +141,7 @@ spec = OpenapiFirst.load('petstore.yaml')
|
|
73
141
|
use OpenapiFirst::Router, spec: spec
|
74
142
|
```
|
75
143
|
|
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
|
144
|
+
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 API description, set `:allow_unknown_operation` to `true`.
|
77
145
|
|
78
146
|
The error responses conform with [JSON:API](https://jsonapi.org).
|
79
147
|
|
@@ -184,7 +252,8 @@ Response validation is to make sure your app responds as described in your OpenA
|
|
184
252
|
require 'openapi_first/response_validator'
|
185
253
|
spec = OpenapiFirst.load('petstore.yaml')
|
186
254
|
validator = OpenapiFirst::ResponseValidator.new(spec)
|
187
|
-
|
255
|
+
|
256
|
+
expect(validator.validate(last_request, last_response).errors).to be_empty
|
188
257
|
```
|
189
258
|
|
190
259
|
TODO: Add RSpec matcher (via extra rubygem)
|
@@ -226,7 +295,7 @@ end
|
|
226
295
|
|
227
296
|
## Mocking
|
228
297
|
|
229
|
-
Currently out of scope.
|
298
|
+
Currently out of scope.
|
230
299
|
|
231
300
|
## Alternatives
|
232
301
|
|
data/lib/openapi_first.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'oas_parser'
|
4
|
+
require 'openapi_first/definition'
|
4
5
|
require 'openapi_first/version'
|
5
6
|
require 'openapi_first/router'
|
6
7
|
require 'openapi_first/query_parameter_validation'
|
@@ -15,7 +16,7 @@ module OpenapiFirst
|
|
15
16
|
QUERY_PARAMS = 'openapi_first.query_params'
|
16
17
|
|
17
18
|
def self.load(spec_path)
|
18
|
-
OasParser::Definition.resolve(spec_path)
|
19
|
+
Definition.new(OasParser::Definition.resolve(spec_path))
|
19
20
|
end
|
20
21
|
|
21
22
|
def self.app(spec, namespace:)
|
@@ -7,29 +7,21 @@ module OpenapiFirst
|
|
7
7
|
def initialize(app, spec)
|
8
8
|
@app = app
|
9
9
|
@spec = spec
|
10
|
-
@to_be_called = spec.
|
11
|
-
endpoint_id(
|
10
|
+
@to_be_called = spec.operations.map do |operation|
|
11
|
+
endpoint_id(operation)
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
15
|
def call(env)
|
16
|
-
|
17
|
-
@to_be_called.delete(endpoint_id(
|
16
|
+
operation = @spec.find_operation(Rack::Request.new(env))
|
17
|
+
@to_be_called.delete(endpoint_id(operation)) if operation
|
18
18
|
@app.call(env)
|
19
19
|
end
|
20
20
|
|
21
21
|
private
|
22
22
|
|
23
|
-
def endpoint_id(
|
24
|
-
"#{
|
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
|
23
|
+
def endpoint_id(operation)
|
24
|
+
"#{operation.path.path}##{operation.method}"
|
33
25
|
end
|
34
26
|
end
|
35
27
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
class Definition
|
5
|
+
def initialize(parsed)
|
6
|
+
@spec = parsed
|
7
|
+
end
|
8
|
+
|
9
|
+
def operations
|
10
|
+
@spec.endpoints
|
11
|
+
end
|
12
|
+
|
13
|
+
def find_operation!(request)
|
14
|
+
@spec
|
15
|
+
.path_by_path(request.path)
|
16
|
+
.endpoint_by_method(request.request_method.downcase)
|
17
|
+
end
|
18
|
+
|
19
|
+
def find_operation(request)
|
20
|
+
find_operation!(request)
|
21
|
+
rescue OasParser::PathNotFound, OasParser::MethodNotFound
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -6,8 +6,8 @@ require_relative 'validation'
|
|
6
6
|
|
7
7
|
module OpenapiFirst
|
8
8
|
class ResponseValidator
|
9
|
-
def initialize(
|
10
|
-
@
|
9
|
+
def initialize(spec)
|
10
|
+
@spec = spec
|
11
11
|
end
|
12
12
|
|
13
13
|
def validate(request, response)
|
@@ -20,7 +20,7 @@ module OpenapiFirst
|
|
20
20
|
private
|
21
21
|
|
22
22
|
def validation_errors(request, response)
|
23
|
-
content = response_for(request, response)
|
23
|
+
content = response_for(request, response)&.content
|
24
24
|
return unless content
|
25
25
|
|
26
26
|
content_type = content[response.content_type]
|
@@ -37,16 +37,23 @@ module OpenapiFirst
|
|
37
37
|
|
38
38
|
def validate_json_schema(schema, data)
|
39
39
|
JSONSchemer.schema(schema).validate(data).to_a.map do |error|
|
40
|
-
error
|
41
|
-
|
40
|
+
format_error(error)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def format_error(error)
|
45
|
+
ValidationFormat.error_details(error)
|
46
|
+
.merge!(
|
47
|
+
data_pointer: error['data_pointer'],
|
48
|
+
schema_pointer: error['schema_pointer']
|
49
|
+
).tap do |formatted|
|
42
50
|
end
|
43
51
|
end
|
44
52
|
|
45
53
|
def response_for(request, response)
|
46
|
-
@
|
47
|
-
.
|
48
|
-
|
49
|
-
.response_by_code(response.status.to_s, use_default: true)
|
54
|
+
@spec
|
55
|
+
.find_operation!(request)
|
56
|
+
&.response_by_code(response.status.to_s, use_default: true)
|
50
57
|
end
|
51
58
|
end
|
52
59
|
end
|
data/lib/openapi_first/router.rb
CHANGED
@@ -15,7 +15,7 @@ module OpenapiFirst
|
|
15
15
|
|
16
16
|
def call(env)
|
17
17
|
req = Rack::Request.new(env)
|
18
|
-
operation = env[OPERATION] = find_operation(req)
|
18
|
+
operation = env[OPERATION] = @spec.find_operation(req)
|
19
19
|
path_params = find_path_params(operation, req)
|
20
20
|
env[PATH_PARAMS] = path_params if path_params
|
21
21
|
return @app.call(env) if operation || @allow_unknown_operation
|
@@ -23,13 +23,6 @@ module OpenapiFirst
|
|
23
23
|
Rack::Response.new('', 404)
|
24
24
|
end
|
25
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
26
|
def find_path_params(operation, req)
|
34
27
|
return unless operation&.path_parameters&.any?
|
35
28
|
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
module OpenapiFirst
|
4
4
|
module ValidationFormat
|
5
|
+
SIMPLE_TYPES = %w[string integer].freeze
|
6
|
+
|
5
7
|
# rubocop:disable Metrics/MethodLength
|
6
8
|
def self.error_details(error)
|
7
9
|
if error['type'] == 'pattern'
|
@@ -14,6 +16,10 @@ module OpenapiFirst
|
|
14
16
|
{
|
15
17
|
title: "is missing required properties: #{missing_keys.join(', ')}"
|
16
18
|
}
|
19
|
+
elsif SIMPLE_TYPES.include?(error['type'])
|
20
|
+
{
|
21
|
+
title: "should be a #{error['type']}"
|
22
|
+
}
|
17
23
|
elsif error['schema'] == false
|
18
24
|
{ title: 'unknown fields are not allowed' }
|
19
25
|
else
|
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.
|
4
|
+
version: 0.6.1
|
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-
|
11
|
+
date: 2019-07-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json_schemer
|
@@ -148,6 +148,7 @@ files:
|
|
148
148
|
- lib/openapi_first.rb
|
149
149
|
- lib/openapi_first/app.rb
|
150
150
|
- lib/openapi_first/coverage.rb
|
151
|
+
- lib/openapi_first/definition.rb
|
151
152
|
- lib/openapi_first/error_response_method.rb
|
152
153
|
- lib/openapi_first/operation_resolver.rb
|
153
154
|
- lib/openapi_first/query_parameter_validation.rb
|