openapi_first 0.11.0 → 0.12.0.alpha1
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 +1 -1
- data/README.md +9 -2
- data/lib/openapi_first.rb +40 -5
- data/lib/openapi_first/app.rb +3 -3
- data/lib/openapi_first/definition.rb +0 -12
- data/lib/openapi_first/request_validation.rb +4 -1
- data/lib/openapi_first/response_validator.rb +9 -6
- data/lib/openapi_first/router.rb +14 -9
- data/lib/openapi_first/version.rb +1 -1
- data/openapi_first.gemspec +7 -7
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 476fbf7b7c6b6d5111d7dcb1fa4cd5197744c989315ea7b82b4e648956d7fee4
|
4
|
+
data.tar.gz: 10e9f751d733284f21eccbad29cf12347ce9924eeba6ed4c4afd43450c952e2e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b0e7387ededf44b1613c8de8663590086ecc063f940f603e52cae799db52600ac257767bbe5c861ad856270a1c4ac9af8d4eb65b8cdc0166fb84f56e05487f8f
|
7
|
+
data.tar.gz: 636a22a59d02af532543e78f46ba7cb2b1acca3f329ff4a75410e3ccedd7f3c95b999bf99c54842d110f0d605827d935737fc58fa4b048c26df44b34f285d46c
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## Unreleased (0.12.x)
|
4
|
+
- Rename `raise` option to `raise_error`
|
5
|
+
- Add `raise_error` option to RequestValidation middleware
|
6
|
+
|
3
7
|
## 0.11.0
|
4
8
|
- Raise error if you forgot to add the Router middleware
|
5
9
|
- Make OpenapiFirst.app raise an error in test env when request path is not specified
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -31,7 +31,7 @@ Options and their defaults:
|
|
31
31
|
|:---|---|---|---|
|
32
32
|
|`spec:`| | The spec loaded via `OpenapiFirst.load` ||
|
33
33
|
| `not_found:` |`nil`, `:continue`, `Proc`| Specifies what to do if the path was not found in the API description. `nil` (default) returns a 404 response. `:continue` does nothing an calls the next app. `Proc` (or something that responds to `call`) to customize the response. | `nil` (return 404)
|
34
|
-
| `
|
34
|
+
| `raise_error:` |`false`, `true` | If set to true the middleware raises `OpenapiFirst::NotFoundError` when a path or method was not found in the API description. This is useful during testing to spot an incomplete API description. | `false` (don't raise an exception)
|
35
35
|
|
36
36
|
## OpenapiFirst::RequestValidation
|
37
37
|
|
@@ -41,6 +41,13 @@ This middleware returns a 400 status code with a body that describes the error i
|
|
41
41
|
use OpenapiFirst::RequestValidation
|
42
42
|
```
|
43
43
|
|
44
|
+
|
45
|
+
Options and their defaults:
|
46
|
+
|
47
|
+
| Name | Possible values | Description | Default
|
48
|
+
|:---|---|---|---|
|
49
|
+
| `raise_error:` |`false`, `true` | If set to true the middleware raises `OpenapiFirst::RequestInvalidError` instead of returning 4xx. | `false` (don't raise an exception)
|
50
|
+
|
44
51
|
The error responses conform with [JSON:API](https://jsonapi.org).
|
45
52
|
|
46
53
|
Here's an example response body for a missing query parameter "search":
|
@@ -117,7 +124,7 @@ There are two ways to set the response body:
|
|
117
124
|
- Returning a value which will get converted to JSON
|
118
125
|
|
119
126
|
## OpenapiFirst::ResponseValidation
|
120
|
-
This middleware is especially useful when testing. It raises an error if the response is not valid.
|
127
|
+
This middleware is especially useful when testing. It *always* raises an error if the response is not valid.
|
121
128
|
|
122
129
|
```ruby
|
123
130
|
use OpenapiFirst::ResponseValidation if ENV['RACK_ENV'] == 'test'
|
data/lib/openapi_first.rb
CHANGED
@@ -19,6 +19,10 @@ module OpenapiFirst
|
|
19
19
|
INBOX = 'openapi_first.inbox'
|
20
20
|
HANDLER = 'openapi_first.handler'
|
21
21
|
|
22
|
+
def self.env
|
23
|
+
ENV['RACK_ENV'] || ENV['HANAMI_ENV'] || ENV['RAILS_ENV']
|
24
|
+
end
|
25
|
+
|
22
26
|
def self.load(spec_path, only: nil)
|
23
27
|
content = YAML.load_file(spec_path)
|
24
28
|
raw = OasParser::Parser.new(spec_path, content).resolve
|
@@ -27,15 +31,14 @@ module OpenapiFirst
|
|
27
31
|
Definition.new(parsed)
|
28
32
|
end
|
29
33
|
|
30
|
-
def self.app(spec, namespace:)
|
34
|
+
def self.app(spec, namespace:, raise_error: OpenapiFirst.env == 'test')
|
31
35
|
spec = OpenapiFirst.load(spec) if spec.is_a?(String)
|
32
|
-
|
33
|
-
App.new(nil, spec, namespace: namespace, router_raise: test)
|
36
|
+
App.new(nil, spec, namespace: namespace, raise_error: raise_error)
|
34
37
|
end
|
35
38
|
|
36
|
-
def self.middleware(spec, namespace:)
|
39
|
+
def self.middleware(spec, namespace:, raise_error: false)
|
37
40
|
spec = OpenapiFirst.load(spec) if spec.is_a?(String)
|
38
|
-
AppWithOptions.new(spec, namespace: namespace,
|
41
|
+
AppWithOptions.new(spec, namespace: namespace, raise_error: raise_error)
|
39
42
|
end
|
40
43
|
|
41
44
|
class AppWithOptions
|
@@ -54,4 +57,36 @@ module OpenapiFirst
|
|
54
57
|
class ResponseCodeNotFoundError < Error; end
|
55
58
|
class ResponseMediaTypeNotFoundError < Error; end
|
56
59
|
class ResponseBodyInvalidError < Error; end
|
60
|
+
|
61
|
+
class RequestInvalidError < Error
|
62
|
+
def initialize(serialized_errors)
|
63
|
+
message = error_message(serialized_errors)
|
64
|
+
super message
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def error_message(errors)
|
70
|
+
errors.map do |error|
|
71
|
+
[human_source(error), human_error(error)].compact.join(' ')
|
72
|
+
end.join(', ')
|
73
|
+
end
|
74
|
+
|
75
|
+
def human_source(error)
|
76
|
+
return unless error[:source]
|
77
|
+
|
78
|
+
source_key = error[:source].keys.first
|
79
|
+
source = {
|
80
|
+
pointer: 'Request body invalid:',
|
81
|
+
parameter: 'Query parameter invalid:'
|
82
|
+
}.fetch(source_key, source_key)
|
83
|
+
name = error[:source].values.first
|
84
|
+
source += " #{name}" unless name.nil? || name.empty?
|
85
|
+
source
|
86
|
+
end
|
87
|
+
|
88
|
+
def human_error(error)
|
89
|
+
error[:title]
|
90
|
+
end
|
91
|
+
end
|
57
92
|
end
|
data/lib/openapi_first/app.rb
CHANGED
@@ -5,11 +5,11 @@ require 'logger'
|
|
5
5
|
|
6
6
|
module OpenapiFirst
|
7
7
|
class App
|
8
|
-
def initialize(parent_app, spec, namespace:,
|
8
|
+
def initialize(parent_app, spec, namespace:, raise_error:)
|
9
9
|
@stack = Rack::Builder.app do
|
10
10
|
freeze_app
|
11
|
-
use OpenapiFirst::Router, spec: spec,
|
12
|
-
use OpenapiFirst::RequestValidation
|
11
|
+
use OpenapiFirst::Router, spec: spec, raise_error: raise_error, parent_app: parent_app
|
12
|
+
use OpenapiFirst::RequestValidation, raise_error: raise_error
|
13
13
|
run OpenapiFirst::Responder.new(
|
14
14
|
spec: spec,
|
15
15
|
namespace: namespace
|
@@ -14,17 +14,5 @@ module OpenapiFirst
|
|
14
14
|
def operations
|
15
15
|
@spec.endpoints.map { |e| Operation.new(e) }
|
16
16
|
end
|
17
|
-
|
18
|
-
def find_operation!(request)
|
19
|
-
@spec
|
20
|
-
.path_by_path(request.path)
|
21
|
-
.endpoint_by_method(request.request_method.downcase)
|
22
|
-
end
|
23
|
-
|
24
|
-
def find_operation(request)
|
25
|
-
find_operation!(request)
|
26
|
-
rescue OasParser::PathNotFound, OasParser::MethodNotFound
|
27
|
-
nil
|
28
|
-
end
|
29
17
|
end
|
30
18
|
end
|
@@ -11,8 +11,9 @@ module OpenapiFirst
|
|
11
11
|
class RequestValidation # rubocop:disable Metrics/ClassLength
|
12
12
|
prepend RouterRequired
|
13
13
|
|
14
|
-
def initialize(app)
|
14
|
+
def initialize(app, raise_error: false)
|
15
15
|
@app = app
|
16
|
+
@raise = raise_error
|
16
17
|
end
|
17
18
|
|
18
19
|
def call(env) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
@@ -85,6 +86,8 @@ module OpenapiFirst
|
|
85
86
|
end
|
86
87
|
|
87
88
|
def error_response(status, errors = [default_error(status)])
|
89
|
+
raise RequestInvalidError, errors if @raise
|
90
|
+
|
88
91
|
Rack::Response.new(
|
89
92
|
MultiJson.dump(errors: errors),
|
90
93
|
status,
|
@@ -3,31 +3,33 @@
|
|
3
3
|
require 'json_schemer'
|
4
4
|
require 'multi_json'
|
5
5
|
require_relative 'validation'
|
6
|
+
require_relative 'router'
|
6
7
|
|
7
8
|
module OpenapiFirst
|
8
9
|
class ResponseValidator
|
9
10
|
def initialize(spec)
|
10
11
|
@spec = spec
|
12
|
+
@router = Router.new(->(_env) {}, spec: spec, raise_error: true)
|
11
13
|
end
|
12
14
|
|
13
15
|
def validate(request, response)
|
14
16
|
errors = validation_errors(request, response)
|
15
17
|
Validation.new(errors || [])
|
16
|
-
rescue
|
18
|
+
rescue OpenapiFirst::ResponseCodeNotFoundError, OpenapiFirst::NotFoundError => e
|
17
19
|
Validation.new([e.message])
|
18
20
|
end
|
19
21
|
|
20
22
|
def validate_operation(request, response)
|
21
23
|
errors = validation_errors(request, response)
|
22
24
|
Validation.new(errors || [])
|
23
|
-
rescue
|
25
|
+
rescue OpenapiFirst::ResponseCodeNotFoundError, OpenapiFirst::NotFoundError => e
|
24
26
|
Validation.new([e.message])
|
25
27
|
end
|
26
28
|
|
27
29
|
private
|
28
30
|
|
29
31
|
def validation_errors(request, response)
|
30
|
-
content = response_for(request, response)&.content
|
32
|
+
content = response_for(request, response)&.fetch('content', nil)
|
31
33
|
return unless content
|
32
34
|
|
33
35
|
content_type = content[response.content_type]
|
@@ -55,9 +57,10 @@ module OpenapiFirst
|
|
55
57
|
end
|
56
58
|
|
57
59
|
def response_for(request, response)
|
58
|
-
|
59
|
-
|
60
|
-
|
60
|
+
env = request.env.dup
|
61
|
+
@router.call(env)
|
62
|
+
operation = env[OPERATION]
|
63
|
+
operation&.response_for(response.status)
|
61
64
|
end
|
62
65
|
end
|
63
66
|
end
|
data/lib/openapi_first/router.rb
CHANGED
@@ -9,24 +9,29 @@ module OpenapiFirst
|
|
9
9
|
NOT_FOUND = Rack::Response.new('', 404).finish.freeze
|
10
10
|
DEFAULT_NOT_FOUND_APP = ->(_env) { NOT_FOUND }
|
11
11
|
|
12
|
-
def initialize(
|
12
|
+
def initialize(
|
13
|
+
app,
|
14
|
+
spec:,
|
15
|
+
raise_error: false,
|
16
|
+
parent_app: nil,
|
17
|
+
not_found: nil
|
18
|
+
)
|
13
19
|
@app = app
|
14
|
-
@parent_app =
|
15
|
-
@raise =
|
16
|
-
@failure_app = find_failure_app(
|
20
|
+
@parent_app = parent_app
|
21
|
+
@raise = raise_error
|
22
|
+
@failure_app = find_failure_app(not_found)
|
17
23
|
if @failure_app.nil?
|
18
24
|
raise ArgumentError,
|
19
25
|
'not_found must be nil, :continue or must respond to call'
|
20
26
|
end
|
21
|
-
spec = options.fetch(:spec)
|
22
27
|
@filepath = spec.filepath
|
23
28
|
@router = build_router(spec.operations)
|
24
29
|
end
|
25
30
|
|
26
31
|
def call(env)
|
27
32
|
env[OPERATION] = nil
|
28
|
-
|
29
|
-
return
|
33
|
+
route = find_route(env)
|
34
|
+
return route.call(env) if route.routable?
|
30
35
|
|
31
36
|
if @raise
|
32
37
|
req = Rack::Request.new(env)
|
@@ -47,10 +52,10 @@ module OpenapiFirst
|
|
47
52
|
option if option.respond_to?(:call)
|
48
53
|
end
|
49
54
|
|
50
|
-
def
|
55
|
+
def find_route(env)
|
51
56
|
original_path_info = env[Rack::PATH_INFO]
|
52
57
|
env[Rack::PATH_INFO] = Rack::Request.new(env).path
|
53
|
-
@router.recognize(env)
|
58
|
+
@router.recognize(env)
|
54
59
|
ensure
|
55
60
|
env[Rack::PATH_INFO] = original_path_info
|
56
61
|
end
|
data/openapi_first.gemspec
CHANGED
@@ -32,13 +32,13 @@ Gem::Specification.new do |spec|
|
|
32
32
|
spec.bindir = 'exe'
|
33
33
|
spec.require_paths = ['lib']
|
34
34
|
|
35
|
-
spec.
|
36
|
-
spec.
|
37
|
-
spec.
|
38
|
-
spec.
|
39
|
-
spec.
|
40
|
-
spec.
|
41
|
-
spec.
|
35
|
+
spec.add_runtime_dependency 'deep_merge', '>= 1.2.1'
|
36
|
+
spec.add_runtime_dependency 'hanami-router', '~> 2.0.alpha3'
|
37
|
+
spec.add_runtime_dependency 'hanami-utils', '~> 2.0.alpha1'
|
38
|
+
spec.add_runtime_dependency 'json_schemer', '~> 0.2'
|
39
|
+
spec.add_runtime_dependency 'multi_json', '~> 1.14'
|
40
|
+
spec.add_runtime_dependency 'oas_parser', '~> 0.25.1'
|
41
|
+
spec.add_runtime_dependency 'rack', '~> 2.2'
|
42
42
|
|
43
43
|
spec.add_development_dependency 'bundler', '~> 2'
|
44
44
|
spec.add_development_dependency 'rack-test', '~> 1'
|
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.
|
4
|
+
version: 0.12.0.alpha1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andreas Haller
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-06-
|
11
|
+
date: 2020-06-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deep_merge
|
@@ -236,9 +236,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
236
236
|
version: '0'
|
237
237
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
238
238
|
requirements:
|
239
|
-
- - "
|
239
|
+
- - ">"
|
240
240
|
- !ruby/object:Gem::Version
|
241
|
-
version:
|
241
|
+
version: 1.3.1
|
242
242
|
requirements: []
|
243
243
|
rubygems_version: 3.1.2
|
244
244
|
signing_key:
|