openapi_first 1.0.0.beta4 → 1.0.0.beta5
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 +5 -0
- data/Gemfile.lock +14 -16
- data/README.md +17 -7
- data/benchmarks/Gemfile.lock +30 -19
- data/lib/openapi_first/body_parser_middleware.rb +4 -17
- data/lib/openapi_first/config.rb +4 -3
- data/lib/openapi_first/error_response.rb +26 -12
- data/lib/openapi_first/error_responses/default.rb +58 -0
- data/lib/openapi_first/error_responses/json_api.rb +58 -0
- data/lib/openapi_first/json_schema/result.rb +17 -0
- data/lib/openapi_first/{schema_validation.rb → json_schema.rb} +6 -6
- data/lib/openapi_first/operation.rb +35 -7
- data/lib/openapi_first/request_body_validator.rb +4 -4
- data/lib/openapi_first/request_validation.rb +39 -33
- data/lib/openapi_first/request_validation_error.rb +31 -0
- data/lib/openapi_first/response_validation.rb +3 -3
- data/lib/openapi_first/response_validator.rb +1 -0
- data/lib/openapi_first/router.rb +7 -15
- data/lib/openapi_first/version.rb +1 -1
- data/lib/openapi_first.rb +2 -13
- data/openapi_first.gemspec +2 -2
- metadata +19 -12
- data/lib/openapi_first/default_error_response.rb +0 -47
- data/lib/openapi_first/operation_schemas.rb +0 -52
- data/lib/openapi_first/validation_result.rb +0 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e6c1f8f1a6fffd91827a74f95b932bfd5be9d4a897f3704efd6313bac9e6be4
|
|
4
|
+
data.tar.gz: 59041cacdcb634bb25e5d23025c0f86afe97632918f1e10d6f67409d31ba5f9b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1955264ba1b60f477123cd1bbb71a14d611d598664965548a9ebe3c6508d5ac6e205dfe971bc7c1ebe6b27da78a48f1bf5d27239c886a9b4aa7db303224e0cfc
|
|
7
|
+
data.tar.gz: 2f25b5944e546a6619c2be01462008d358a0a80140594b0906ca62e9b152fa97b2b8225d0c137acbe29c64b7732bbd00d3f35459cd0b771b2b38c409303adde8
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.0.0.beta5
|
|
6
|
+
|
|
7
|
+
- Added: `OpenapiFirst::Config.default_options=` to set default options globally
|
|
8
|
+
- Added: You can define custom error responses by subclassing `OpenapiFirst::ErrorResponse` and register it via `OpenapiFirst::Plugins.register_error_response(name, MyCustomErrorResponse)`
|
|
9
|
+
|
|
5
10
|
## 1.0.0.beta4
|
|
6
11
|
|
|
7
12
|
- Update json_schemer to version 2.0
|
data/Gemfile.lock
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
openapi_first (1.0.0.
|
|
4
|
+
openapi_first (1.0.0.beta5)
|
|
5
5
|
hanami-router (~> 2.0.0)
|
|
6
6
|
json_refs (~> 0.1, >= 0.1.7)
|
|
7
7
|
json_schemer (~> 2.0.0)
|
|
8
|
-
multi_json (~> 1.
|
|
9
|
-
openapi_parameters (
|
|
8
|
+
multi_json (~> 1.15)
|
|
9
|
+
openapi_parameters (>= 0.3.1, < 2.0)
|
|
10
10
|
rack (>= 2.2, < 4.0)
|
|
11
11
|
|
|
12
12
|
GEM
|
|
13
13
|
remote: https://rubygems.org/
|
|
14
14
|
specs:
|
|
15
15
|
ast (2.4.2)
|
|
16
|
-
base64 (0.1.1)
|
|
17
16
|
diff-lcs (1.5.0)
|
|
18
17
|
hana (1.3.7)
|
|
19
18
|
hanami-router (2.0.2)
|
|
@@ -35,20 +34,20 @@ GEM
|
|
|
35
34
|
mustermann-contrib (3.0.0)
|
|
36
35
|
hansi (~> 0.2.0)
|
|
37
36
|
mustermann (= 3.0.0)
|
|
38
|
-
openapi_parameters (0.
|
|
37
|
+
openapi_parameters (0.3.1)
|
|
39
38
|
rack (>= 2.2)
|
|
40
39
|
zeitwerk (~> 2.6)
|
|
41
40
|
parallel (1.23.0)
|
|
42
|
-
parser (3.2.2.
|
|
41
|
+
parser (3.2.2.4)
|
|
43
42
|
ast (~> 2.4.1)
|
|
44
43
|
racc
|
|
45
|
-
racc (1.7.
|
|
44
|
+
racc (1.7.3)
|
|
46
45
|
rack (2.2.8)
|
|
47
46
|
rack-test (2.1.0)
|
|
48
47
|
rack (>= 1.3)
|
|
49
48
|
rainbow (3.1.1)
|
|
50
|
-
rake (13.0
|
|
51
|
-
regexp_parser (2.8.
|
|
49
|
+
rake (13.1.0)
|
|
50
|
+
regexp_parser (2.8.2)
|
|
52
51
|
rexml (3.2.6)
|
|
53
52
|
rspec (3.12.0)
|
|
54
53
|
rspec-core (~> 3.12.0)
|
|
@@ -63,19 +62,18 @@ GEM
|
|
|
63
62
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
64
63
|
rspec-support (~> 3.12.0)
|
|
65
64
|
rspec-support (3.12.1)
|
|
66
|
-
rubocop (1.
|
|
67
|
-
base64 (~> 0.1.1)
|
|
65
|
+
rubocop (1.57.2)
|
|
68
66
|
json (~> 2.3)
|
|
69
67
|
language_server-protocol (>= 3.17.0)
|
|
70
68
|
parallel (~> 1.10)
|
|
71
|
-
parser (>= 3.2.2.
|
|
69
|
+
parser (>= 3.2.2.4)
|
|
72
70
|
rainbow (>= 2.2.2, < 4.0)
|
|
73
71
|
regexp_parser (>= 1.8, < 3.0)
|
|
74
72
|
rexml (>= 3.2.5, < 4.0)
|
|
75
73
|
rubocop-ast (>= 1.28.1, < 2.0)
|
|
76
74
|
ruby-progressbar (~> 1.7)
|
|
77
75
|
unicode-display_width (>= 2.4.0, < 3.0)
|
|
78
|
-
rubocop-ast (1.
|
|
76
|
+
rubocop-ast (1.30.0)
|
|
79
77
|
parser (>= 3.2.1.0)
|
|
80
78
|
ruby-progressbar (1.13.0)
|
|
81
79
|
ruby2_keywords (0.0.5)
|
|
@@ -83,9 +81,9 @@ GEM
|
|
|
83
81
|
unf (~> 0.1.4)
|
|
84
82
|
unf (0.1.4)
|
|
85
83
|
unf_ext
|
|
86
|
-
unf_ext (0.0.
|
|
87
|
-
unicode-display_width (2.
|
|
88
|
-
zeitwerk (2.6.
|
|
84
|
+
unf_ext (0.0.9)
|
|
85
|
+
unicode-display_width (2.5.0)
|
|
86
|
+
zeitwerk (2.6.12)
|
|
89
87
|
|
|
90
88
|
PLATFORMS
|
|
91
89
|
arm64-darwin-21
|
data/README.md
CHANGED
|
@@ -28,11 +28,11 @@ It adds these fields to the Rack env:
|
|
|
28
28
|
|
|
29
29
|
### Options and defaults
|
|
30
30
|
|
|
31
|
-
| Name
|
|
32
|
-
|
|
|
33
|
-
| `spec:`
|
|
34
|
-
| `raise_error:`
|
|
35
|
-
| `error_response
|
|
31
|
+
| Name | Possible values | Description | Default |
|
|
32
|
+
| :---------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------- |
|
|
33
|
+
| `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` |
|
|
34
|
+
| `raise_error:` | `false`, `true` | If set to true the middleware raises `OpenapiFirst::RequestInvalidError` instead of returning 4xx. | `false` (don't raise an exception) |
|
|
35
|
+
| `error_response:` | `:default`, `:json_api`, Your implementation of `ErrorResponse` | :default |
|
|
36
36
|
|
|
37
37
|
The error responses conform with [JSON:API](https://jsonapi.org).
|
|
38
38
|
|
|
@@ -40,7 +40,7 @@ Here's an example response body for a missing query parameter "search":
|
|
|
40
40
|
|
|
41
41
|
```json
|
|
42
42
|
http-status: 400
|
|
43
|
-
content-type: "application/
|
|
43
|
+
content-type: "application/json"
|
|
44
44
|
|
|
45
45
|
{
|
|
46
46
|
"errors": [
|
|
@@ -54,7 +54,6 @@ content-type: "application/vnd.api+json"
|
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
|
|
58
57
|
### Parameters
|
|
59
58
|
|
|
60
59
|
The `RequestValidation` middleware adds `env[OpenapiFirst::PARAMS]` (or `env['openapi.params']` ) with the converted query and path parameters. This only includes the parameters that are defined in the API description. It supports every [`style` and `explode` value as described](https://spec.openapis.org/oas/latest.html#style-examples) in the OpenAPI 3.0 and 3.1 specs. So you can do things these:
|
|
@@ -120,6 +119,17 @@ This middleware adds `env['openapi.operation']` which holds an instance of `Open
|
|
|
120
119
|
| `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) |
|
|
121
120
|
| `not_found:` | `:continue`, `:halt` | If set to `:continue` the middleware will not return 404 (405, 415), but just pass handling the request to the next middleware or application in the Rack stack. If combined with `raise_error: true` `raise_error` gets preference and an exception is raised. | `:halt` (return 4xx response) |
|
|
122
121
|
|
|
122
|
+
## Global configuration
|
|
123
|
+
|
|
124
|
+
You can configure default options gobally via `OpenapiFirst::Config`:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
OpenapiFirst::Config.default_options = {
|
|
128
|
+
error_response: :json_api,
|
|
129
|
+
request_validation_raise_error: true
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
123
133
|
## Alternatives
|
|
124
134
|
|
|
125
135
|
This gem is inspired by [committee](https://github.com/interagent/committee) (Ruby) and [connexion](https://github.com/zalando/connexion) (Python).
|
data/benchmarks/Gemfile.lock
CHANGED
|
@@ -1,32 +1,42 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
openapi_first (1.0.0.
|
|
4
|
+
openapi_first (1.0.0.beta5)
|
|
5
5
|
hanami-router (~> 2.0.0)
|
|
6
6
|
json_refs (~> 0.1, >= 0.1.7)
|
|
7
7
|
json_schemer (~> 2.0.0)
|
|
8
|
-
multi_json (~> 1.
|
|
9
|
-
openapi_parameters (
|
|
8
|
+
multi_json (~> 1.15)
|
|
9
|
+
openapi_parameters (>= 0.3.1, < 2.0)
|
|
10
10
|
rack (>= 2.2, < 4.0)
|
|
11
11
|
|
|
12
12
|
GEM
|
|
13
13
|
remote: https://rubygems.org/
|
|
14
14
|
specs:
|
|
15
|
-
activesupport (7.
|
|
15
|
+
activesupport (7.1.2)
|
|
16
|
+
base64
|
|
17
|
+
bigdecimal
|
|
16
18
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
19
|
+
connection_pool (>= 2.2.5)
|
|
20
|
+
drb
|
|
17
21
|
i18n (>= 1.6, < 2)
|
|
18
22
|
minitest (>= 5.1)
|
|
23
|
+
mutex_m
|
|
19
24
|
tzinfo (~> 2.0)
|
|
25
|
+
base64 (0.2.0)
|
|
20
26
|
benchmark-ips (2.12.0)
|
|
21
27
|
benchmark-memory (0.2.0)
|
|
22
28
|
memory_profiler (~> 1)
|
|
29
|
+
bigdecimal (3.1.4)
|
|
23
30
|
builder (3.2.4)
|
|
24
31
|
committee (5.0.0)
|
|
25
32
|
json_schema (~> 0.14, >= 0.14.3)
|
|
26
33
|
openapi_parser (~> 1.0)
|
|
27
34
|
rack (>= 1.5)
|
|
28
35
|
concurrent-ruby (1.2.2)
|
|
29
|
-
|
|
36
|
+
connection_pool (2.4.1)
|
|
37
|
+
drb (2.2.0)
|
|
38
|
+
ruby2_keywords
|
|
39
|
+
dry-core (1.0.1)
|
|
30
40
|
concurrent-ruby (~> 1.0)
|
|
31
41
|
zeitwerk (~> 2.6)
|
|
32
42
|
dry-inflector (1.0.0)
|
|
@@ -40,12 +50,12 @@ GEM
|
|
|
40
50
|
dry-inflector (~> 1.0)
|
|
41
51
|
dry-logic (~> 1.4)
|
|
42
52
|
zeitwerk (~> 2.6)
|
|
43
|
-
grape (
|
|
44
|
-
activesupport
|
|
53
|
+
grape (2.0.0)
|
|
54
|
+
activesupport (>= 5)
|
|
45
55
|
builder
|
|
46
56
|
dry-types (>= 1.1)
|
|
47
57
|
mustermann-grape (~> 1.0.0)
|
|
48
|
-
rack (>= 1.3.0
|
|
58
|
+
rack (>= 1.3.0)
|
|
49
59
|
rack-accept
|
|
50
60
|
hana (1.3.7)
|
|
51
61
|
hanami-api (0.3.0)
|
|
@@ -74,40 +84,41 @@ GEM
|
|
|
74
84
|
mustermann (= 3.0.0)
|
|
75
85
|
mustermann-grape (1.0.2)
|
|
76
86
|
mustermann (>= 1.0.0)
|
|
87
|
+
mutex_m (0.2.0)
|
|
77
88
|
nio4r (2.5.9)
|
|
78
|
-
openapi_parameters (0.
|
|
89
|
+
openapi_parameters (0.3.1)
|
|
79
90
|
rack (>= 2.2)
|
|
80
91
|
zeitwerk (~> 2.6)
|
|
81
92
|
openapi_parser (1.0.0)
|
|
82
|
-
puma (6.
|
|
93
|
+
puma (6.4.0)
|
|
83
94
|
nio4r (~> 2.0)
|
|
84
95
|
rack (2.2.8)
|
|
85
96
|
rack-accept (0.4.5)
|
|
86
97
|
rack (>= 0.4)
|
|
87
|
-
rack-protection (3.0
|
|
88
|
-
rack
|
|
89
|
-
regexp_parser (2.8.
|
|
90
|
-
roda (3.
|
|
98
|
+
rack-protection (3.1.0)
|
|
99
|
+
rack (~> 2.2, >= 2.2.4)
|
|
100
|
+
regexp_parser (2.8.2)
|
|
101
|
+
roda (3.73.0)
|
|
91
102
|
rack
|
|
92
103
|
ruby2_keywords (0.0.5)
|
|
93
104
|
seg (1.2.0)
|
|
94
105
|
simpleidn (0.2.1)
|
|
95
106
|
unf (~> 0.1.4)
|
|
96
|
-
sinatra (3.0
|
|
107
|
+
sinatra (3.1.0)
|
|
97
108
|
mustermann (~> 3.0)
|
|
98
109
|
rack (~> 2.2, >= 2.2.4)
|
|
99
|
-
rack-protection (= 3.0
|
|
110
|
+
rack-protection (= 3.1.0)
|
|
100
111
|
tilt (~> 2.0)
|
|
101
112
|
syro (3.2.1)
|
|
102
113
|
rack (>= 1.6.0)
|
|
103
114
|
seg
|
|
104
|
-
tilt (2.
|
|
115
|
+
tilt (2.3.0)
|
|
105
116
|
tzinfo (2.0.6)
|
|
106
117
|
concurrent-ruby (~> 1.0)
|
|
107
118
|
unf (0.1.4)
|
|
108
119
|
unf_ext
|
|
109
|
-
unf_ext (0.0.
|
|
110
|
-
zeitwerk (2.6.
|
|
120
|
+
unf_ext (0.0.9)
|
|
121
|
+
zeitwerk (2.6.12)
|
|
111
122
|
|
|
112
123
|
PLATFORMS
|
|
113
124
|
arm64-darwin-21
|
|
@@ -4,29 +4,16 @@ require 'multi_json'
|
|
|
4
4
|
|
|
5
5
|
module OpenapiFirst
|
|
6
6
|
class BodyParserMiddleware
|
|
7
|
-
def initialize(app
|
|
7
|
+
def initialize(app)
|
|
8
8
|
@app = app
|
|
9
|
-
@raise = options.fetch(:raise_error, false)
|
|
10
9
|
end
|
|
11
10
|
|
|
12
|
-
RACK_INPUT = 'rack.input'
|
|
13
11
|
ROUTER_PARSED_BODY = 'router.parsed_body'
|
|
12
|
+
private_constant :ROUTER_PARSED_BODY
|
|
14
13
|
|
|
15
14
|
def call(env)
|
|
16
15
|
env[ROUTER_PARSED_BODY] = parse_body(env)
|
|
17
16
|
@app.call(env)
|
|
18
|
-
rescue BodyParsingError => e
|
|
19
|
-
raise if @raise
|
|
20
|
-
|
|
21
|
-
err = { title: "Failed to parse body as #{env['CONTENT_TYPE']}", status: '400' }
|
|
22
|
-
err[:detail] = e.cause unless ENV['RACK_ENV'] == 'production'
|
|
23
|
-
errors = [err]
|
|
24
|
-
|
|
25
|
-
Rack::Response.new(
|
|
26
|
-
MultiJson.dump(errors:),
|
|
27
|
-
400,
|
|
28
|
-
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
|
29
|
-
).finish
|
|
30
17
|
end
|
|
31
18
|
|
|
32
19
|
private
|
|
@@ -40,8 +27,8 @@ module OpenapiFirst
|
|
|
40
27
|
return request.POST if request.form_data?
|
|
41
28
|
|
|
42
29
|
body
|
|
43
|
-
rescue MultiJson::ParseError
|
|
44
|
-
raise BodyParsingError,
|
|
30
|
+
rescue MultiJson::ParseError
|
|
31
|
+
raise BodyParsingError, 'Failed to parse body as application/json'
|
|
45
32
|
end
|
|
46
33
|
|
|
47
34
|
def read_body(request)
|
data/lib/openapi_first/config.rb
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module OpenapiFirst
|
|
4
4
|
class Config
|
|
5
|
-
def initialize(error_response: :default)
|
|
6
|
-
@error_response = error_response
|
|
5
|
+
def initialize(error_response: :default, request_validation_raise_error: false)
|
|
6
|
+
@error_response = Plugins.find_error_response(error_response)
|
|
7
|
+
@request_validation_raise_error = request_validation_raise_error
|
|
7
8
|
end
|
|
8
9
|
|
|
9
|
-
attr_reader :error_response
|
|
10
|
+
attr_reader :error_response, :request_validation_raise_error
|
|
10
11
|
|
|
11
12
|
def self.default_options
|
|
12
13
|
@default_options ||= new
|
|
@@ -3,20 +3,34 @@
|
|
|
3
3
|
module OpenapiFirst
|
|
4
4
|
# This is the base class for error responses
|
|
5
5
|
class ErrorResponse
|
|
6
|
-
## @param
|
|
7
|
-
## @param
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@status = status
|
|
12
|
-
@title = title
|
|
13
|
-
@location = location
|
|
14
|
-
@validation_output = validation_result&.output
|
|
15
|
-
@schema = validation_result&.schema
|
|
16
|
-
@data = validation_result&.data
|
|
6
|
+
## @param request [Hash] The Rack request env
|
|
7
|
+
## @param request_validation_error [OpenapiFirst::RequestValidationError]
|
|
8
|
+
def initialize(env, request_validation_error)
|
|
9
|
+
@env = env
|
|
10
|
+
@request_validation_error = request_validation_error
|
|
17
11
|
end
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
extend Forwardable
|
|
14
|
+
|
|
15
|
+
attr_reader :env, :request_validation_error
|
|
16
|
+
|
|
17
|
+
def_delegators :@request_validation_error, :status, :location, :schema_validation
|
|
18
|
+
|
|
19
|
+
def validation_output
|
|
20
|
+
schema_validation&.output
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def schema
|
|
24
|
+
schema_validation&.schema
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def data
|
|
28
|
+
schema_validation&.data
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def message
|
|
32
|
+
request_validation_error.message
|
|
33
|
+
end
|
|
20
34
|
|
|
21
35
|
def render
|
|
22
36
|
Rack::Response.new(body, status, Rack::CONTENT_TYPE => content_type).finish
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiFirst
|
|
4
|
+
module ErrorResponses
|
|
5
|
+
class Default < ErrorResponse
|
|
6
|
+
OpenapiFirst::Plugins.register_error_response(:default, self)
|
|
7
|
+
|
|
8
|
+
def body
|
|
9
|
+
MultiJson.dump({ errors: serialized_errors })
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def content_type
|
|
13
|
+
'application/json'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def serialized_errors
|
|
17
|
+
return default_errors unless validation_output
|
|
18
|
+
|
|
19
|
+
key = pointer_key
|
|
20
|
+
validation_errors&.map do |error|
|
|
21
|
+
{
|
|
22
|
+
status: status.to_s,
|
|
23
|
+
source: { key => pointer(error['instanceLocation']) },
|
|
24
|
+
title: error['error']
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def validation_errors
|
|
30
|
+
validation_output['errors'] || [validation_output]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def default_errors
|
|
34
|
+
[{
|
|
35
|
+
status: status.to_s,
|
|
36
|
+
title: message
|
|
37
|
+
}]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def pointer_key
|
|
41
|
+
case location
|
|
42
|
+
when :body
|
|
43
|
+
:pointer
|
|
44
|
+
when :query, :path
|
|
45
|
+
:parameter
|
|
46
|
+
else
|
|
47
|
+
location
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def pointer(data_pointer)
|
|
52
|
+
return data_pointer if location == :body
|
|
53
|
+
|
|
54
|
+
data_pointer.delete_prefix('/')
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiFirst
|
|
4
|
+
module ErrorResponses
|
|
5
|
+
class JsonApi < ErrorResponse
|
|
6
|
+
OpenapiFirst::Plugins.register_error_response(:json_api, self)
|
|
7
|
+
|
|
8
|
+
def body
|
|
9
|
+
MultiJson.dump({ errors: serialized_errors })
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def content_type
|
|
13
|
+
'application/vnd.api+json'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def serialized_errors
|
|
17
|
+
return default_errors unless validation_output
|
|
18
|
+
|
|
19
|
+
key = pointer_key
|
|
20
|
+
validation_errors&.map do |error|
|
|
21
|
+
{
|
|
22
|
+
status: status.to_s,
|
|
23
|
+
source: { key => pointer(error['instanceLocation']) },
|
|
24
|
+
title: error['error']
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def validation_errors
|
|
30
|
+
validation_output['errors'] || [validation_output]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def default_errors
|
|
34
|
+
[{
|
|
35
|
+
status: status.to_s,
|
|
36
|
+
title: message
|
|
37
|
+
}]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def pointer_key
|
|
41
|
+
case location
|
|
42
|
+
when :body
|
|
43
|
+
:pointer
|
|
44
|
+
when :query, :path
|
|
45
|
+
:parameter
|
|
46
|
+
else
|
|
47
|
+
location
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def pointer(data_pointer)
|
|
52
|
+
return data_pointer if location == :body
|
|
53
|
+
|
|
54
|
+
data_pointer.delete_prefix('/')
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiFirst
|
|
4
|
+
class JsonSchema
|
|
5
|
+
Result = Struct.new(:output, :schema, :data, keyword_init: true) do
|
|
6
|
+
def valid? = output['valid']
|
|
7
|
+
def error? = !output['valid']
|
|
8
|
+
|
|
9
|
+
# Returns a message that is used in exception messages.
|
|
10
|
+
def message
|
|
11
|
+
return if valid?
|
|
12
|
+
|
|
13
|
+
(output['errors']&.map { |e| e['error'] }&.join('. ') || output['error'])
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json_schemer'
|
|
4
|
-
require_relative '
|
|
4
|
+
require_relative 'json_schema/result'
|
|
5
5
|
|
|
6
6
|
module OpenapiFirst
|
|
7
|
-
class
|
|
8
|
-
attr_reader :
|
|
7
|
+
class JsonSchema
|
|
8
|
+
attr_reader :schema
|
|
9
9
|
|
|
10
10
|
SCHEMAS = {
|
|
11
11
|
'3.1' => 'https://spec.openapis.org/oas/3.1/dialect/base',
|
|
@@ -13,7 +13,7 @@ module OpenapiFirst
|
|
|
13
13
|
}.freeze
|
|
14
14
|
|
|
15
15
|
def initialize(schema, openapi_version:, write: true)
|
|
16
|
-
@
|
|
16
|
+
@schema = schema
|
|
17
17
|
@schemer = JSONSchemer.schema(
|
|
18
18
|
schema,
|
|
19
19
|
access_mode: write ? 'write' : 'read',
|
|
@@ -25,9 +25,9 @@ module OpenapiFirst
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def validate(data)
|
|
28
|
-
|
|
28
|
+
Result.new(
|
|
29
29
|
output: @schemer.validate(data),
|
|
30
|
-
schema
|
|
30
|
+
schema:,
|
|
31
31
|
data:
|
|
32
32
|
)
|
|
33
33
|
end
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'forwardable'
|
|
4
4
|
require 'set'
|
|
5
|
-
require_relative '
|
|
6
|
-
require_relative 'operation_schemas'
|
|
5
|
+
require_relative 'json_schema'
|
|
7
6
|
|
|
8
7
|
module OpenapiFirst
|
|
9
8
|
class Operation # rubocop:disable Metrics/ClassLength
|
|
@@ -55,7 +54,7 @@ module OpenapiFirst
|
|
|
55
54
|
schema = media_type['schema']
|
|
56
55
|
return unless schema
|
|
57
56
|
|
|
58
|
-
|
|
57
|
+
JsonSchema.new(schema, write: false, openapi_version:)
|
|
59
58
|
end
|
|
60
59
|
|
|
61
60
|
def request_body_schema(request_content_type)
|
|
@@ -63,7 +62,7 @@ module OpenapiFirst
|
|
|
63
62
|
content = operation_object.dig('requestBody', 'content')
|
|
64
63
|
media_type = find_content_for_content_type(content, request_content_type)
|
|
65
64
|
schema = media_type&.fetch('schema', nil)
|
|
66
|
-
|
|
65
|
+
JsonSchema.new(schema, write: write?, openapi_version:) if schema
|
|
67
66
|
end
|
|
68
67
|
end
|
|
69
68
|
|
|
@@ -114,13 +113,42 @@ module OpenapiFirst
|
|
|
114
113
|
end
|
|
115
114
|
end
|
|
116
115
|
|
|
117
|
-
#
|
|
118
|
-
def
|
|
119
|
-
@
|
|
116
|
+
# Return JSON Schema of for all query parameters
|
|
117
|
+
def query_parameters_schema
|
|
118
|
+
@query_parameters_schema ||= build_json_schema(query_parameters)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Return JSON Schema of for all path parameters
|
|
122
|
+
def path_parameters_schema
|
|
123
|
+
@path_parameters_schema ||= build_json_schema(path_parameters)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def header_parameters_schema
|
|
127
|
+
@header_parameters_schema ||= build_json_schema(header_parameters)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def cookie_parameters_schema
|
|
131
|
+
@cookie_parameters_schema ||= build_json_schema(cookie_parameters)
|
|
120
132
|
end
|
|
121
133
|
|
|
122
134
|
private
|
|
123
135
|
|
|
136
|
+
# Build JSON Schema for given parameter definitions
|
|
137
|
+
# @parameter_defs [Array<Hash>] Parameter definitions
|
|
138
|
+
def build_json_schema(parameter_defs)
|
|
139
|
+
init_schema = {
|
|
140
|
+
'type' => 'object',
|
|
141
|
+
'properties' => {},
|
|
142
|
+
'required' => []
|
|
143
|
+
}
|
|
144
|
+
schema = parameter_defs.each_with_object(init_schema) do |parameter_def, result|
|
|
145
|
+
parameter = OpenapiParameters::Parameter.new(parameter_def)
|
|
146
|
+
result['properties'][parameter.name] = parameter.schema if parameter.schema
|
|
147
|
+
result['required'] << parameter.name if parameter.required?
|
|
148
|
+
end
|
|
149
|
+
JsonSchema.new(schema, openapi_version:)
|
|
150
|
+
end
|
|
151
|
+
|
|
124
152
|
def response_by_code(status)
|
|
125
153
|
operation_object.dig('responses', status.to_s) ||
|
|
126
154
|
operation_object.dig('responses', "#{status / 100}XX") ||
|
|
@@ -17,7 +17,7 @@ module OpenapiFirst
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
19
|
def validate_request_content_type!(operation, content_type)
|
|
20
|
-
operation.valid_request_content_type?(content_type) ||
|
|
20
|
+
operation.valid_request_content_type?(content_type) || RequestValidation.fail!(415, :header)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def validate_request_body!(operation, body, content_type)
|
|
@@ -27,15 +27,15 @@ module OpenapiFirst
|
|
|
27
27
|
schema = operation&.request_body_schema(content_type)
|
|
28
28
|
return unless schema
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
schema_validation = schema.validate(body)
|
|
31
|
+
RequestValidation.fail!(400, :body, schema_validation:) if schema_validation.error?
|
|
32
32
|
body
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def validate_request_body_presence!(body, operation)
|
|
36
36
|
return unless operation.request_body['required'] && body.nil?
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
RequestValidation.fail!(400, :body)
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
end
|
|
@@ -6,15 +6,37 @@ require_relative 'use_router'
|
|
|
6
6
|
require_relative 'error_response'
|
|
7
7
|
require_relative 'request_body_validator'
|
|
8
8
|
require_relative 'string_keyed_hash'
|
|
9
|
+
require_relative 'request_validation_error'
|
|
9
10
|
require 'openapi_parameters'
|
|
10
11
|
|
|
11
12
|
module OpenapiFirst
|
|
13
|
+
# A Rack middleware to validate requests against an OpenAPI API description
|
|
12
14
|
class RequestValidation
|
|
13
15
|
prepend UseRouter
|
|
14
16
|
|
|
17
|
+
FAIL = :request_validation_failed
|
|
18
|
+
private_constant :FAIL
|
|
19
|
+
|
|
20
|
+
# @param status [Integer] The intended HTTP status code (usually 400)
|
|
21
|
+
# @param location [Symbol] One of :body, :header, :cookie, :query, :path
|
|
22
|
+
# @param schema_validation [OpenapiFirst::JsonSchema::Result]
|
|
23
|
+
def self.fail!(status, location, schema_validation: nil)
|
|
24
|
+
throw FAIL, RequestValidationError.new(
|
|
25
|
+
status:,
|
|
26
|
+
location:,
|
|
27
|
+
schema_validation:
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param app The parent Rack application
|
|
32
|
+
# @param options An optional Hash of configuration options to override defaults
|
|
33
|
+
# :error_response A Boolean indicating whether to raise an error if validation fails.
|
|
34
|
+
# default: OpenapiFirst::ErrorResponses::Default (Config.default_options.error_response)
|
|
35
|
+
# :raise_error The Class to use for error responses.
|
|
36
|
+
# default: false (Config.default_options.request_validation_raise_error)
|
|
15
37
|
def initialize(app, options = {})
|
|
16
38
|
@app = app
|
|
17
|
-
@raise = options.fetch(:raise_error,
|
|
39
|
+
@raise = options.fetch(:raise_error, Config.default_options.request_validation_raise_error)
|
|
18
40
|
@error_response_class =
|
|
19
41
|
Plugins.find_error_response(options.fetch(:error_response, Config.default_options.error_response))
|
|
20
42
|
end
|
|
@@ -25,43 +47,23 @@ module OpenapiFirst
|
|
|
25
47
|
|
|
26
48
|
error = validate_request(operation, env)
|
|
27
49
|
if error
|
|
28
|
-
|
|
29
|
-
raise RequestInvalidError, error_message(title, location) if @raise
|
|
50
|
+
raise RequestInvalidError, error.error_message if @raise
|
|
30
51
|
|
|
31
|
-
return
|
|
52
|
+
return @error_response_class.new(env, error).render
|
|
32
53
|
end
|
|
33
54
|
@app.call(env)
|
|
34
55
|
end
|
|
35
56
|
|
|
36
57
|
private
|
|
37
58
|
|
|
38
|
-
def error_message(title, location)
|
|
39
|
-
return title unless location
|
|
40
|
-
|
|
41
|
-
"#{TOPICS.fetch(location)} #{title}"
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
TOPICS = {
|
|
45
|
-
request_body: 'Request body invalid:',
|
|
46
|
-
query: 'Query parameter invalid:',
|
|
47
|
-
header: 'Header parameter invalid:',
|
|
48
|
-
path: 'Path segment invalid:',
|
|
49
|
-
cookie: 'Cookie value invalid:'
|
|
50
|
-
}.freeze
|
|
51
|
-
private_constant :TOPICS
|
|
52
|
-
|
|
53
|
-
def error_response(error_object)
|
|
54
|
-
@error_response_class.new(**error_object)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
59
|
def validate_request(operation, env)
|
|
58
|
-
catch(
|
|
60
|
+
catch(FAIL) do
|
|
59
61
|
env[PARAMS] = {}
|
|
60
62
|
validate_query_params!(operation, env)
|
|
61
63
|
validate_path_params!(operation, env)
|
|
62
64
|
validate_cookie_params!(operation, env)
|
|
63
65
|
validate_header_params!(operation, env)
|
|
64
|
-
|
|
66
|
+
validate_request_body!(operation, env)
|
|
65
67
|
nil
|
|
66
68
|
end
|
|
67
69
|
end
|
|
@@ -72,8 +74,8 @@ module OpenapiFirst
|
|
|
72
74
|
|
|
73
75
|
hashy = StringKeyedHash.new(env[Router::RAW_PATH_PARAMS])
|
|
74
76
|
unpacked_path_params = OpenapiParameters::Path.new(path_parameters).unpack(hashy)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
schema_validation = operation.path_parameters_schema.validate(unpacked_path_params)
|
|
78
|
+
RequestValidation.fail!(400, :path, schema_validation:) if schema_validation.error?
|
|
77
79
|
env[PATH_PARAMS] = unpacked_path_params
|
|
78
80
|
env[PARAMS].merge!(unpacked_path_params)
|
|
79
81
|
end
|
|
@@ -83,8 +85,8 @@ module OpenapiFirst
|
|
|
83
85
|
return if operation.query_parameters.empty?
|
|
84
86
|
|
|
85
87
|
unpacked_query_params = OpenapiParameters::Query.new(query_parameters).unpack(env['QUERY_STRING'])
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
schema_validation = operation.query_parameters_schema.validate(unpacked_query_params)
|
|
89
|
+
RequestValidation.fail!(400, :query, schema_validation:) if schema_validation.error?
|
|
88
90
|
env[QUERY_PARAMS] = unpacked_query_params
|
|
89
91
|
env[PARAMS].merge!(unpacked_query_params)
|
|
90
92
|
end
|
|
@@ -94,8 +96,8 @@ module OpenapiFirst
|
|
|
94
96
|
return unless cookie_parameters&.any?
|
|
95
97
|
|
|
96
98
|
unpacked_params = OpenapiParameters::Cookie.new(cookie_parameters).unpack(env['HTTP_COOKIE'])
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
schema_validation = operation.cookie_parameters_schema.validate(unpacked_params)
|
|
100
|
+
RequestValidation.fail!(400, :cookie, schema_validation:) if schema_validation.error?
|
|
99
101
|
env[COOKIE_PARAMS] = unpacked_params
|
|
100
102
|
end
|
|
101
103
|
|
|
@@ -104,9 +106,13 @@ module OpenapiFirst
|
|
|
104
106
|
return if header_parameters.empty?
|
|
105
107
|
|
|
106
108
|
unpacked_header_params = OpenapiParameters::Header.new(header_parameters).unpack_env(env)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
schema_validation = operation.header_parameters_schema.validate(unpacked_header_params)
|
|
110
|
+
RequestValidation.fail!(400, :header, schema_validation:) if schema_validation.error?
|
|
109
111
|
env[HEADER_PARAMS] = unpacked_header_params
|
|
110
112
|
end
|
|
113
|
+
|
|
114
|
+
def validate_request_body!(operation, env)
|
|
115
|
+
RequestBodyValidator.new(operation, env).validate! if operation.request_body
|
|
116
|
+
end
|
|
111
117
|
end
|
|
112
118
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiFirst
|
|
4
|
+
class RequestValidationError
|
|
5
|
+
def initialize(status:, location:, message: nil, schema_validation: nil)
|
|
6
|
+
@status = status
|
|
7
|
+
@location = location
|
|
8
|
+
@message = message
|
|
9
|
+
@schema_validation = schema_validation
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :status, :request, :location, :schema_validation
|
|
13
|
+
|
|
14
|
+
def message
|
|
15
|
+
@message || schema_validation&.message || Rack::Utils::HTTP_STATUS_CODES[status]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def error_message
|
|
19
|
+
"#{TOPICS.fetch(location)} #{message}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
TOPICS = {
|
|
23
|
+
body: 'Request body invalid:',
|
|
24
|
+
query: 'Query parameter invalid:',
|
|
25
|
+
header: 'Header parameter invalid:',
|
|
26
|
+
path: 'Path segment invalid:',
|
|
27
|
+
cookie: 'Cookie value invalid:'
|
|
28
|
+
}.freeze
|
|
29
|
+
private_constant :TOPICS
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -65,10 +65,10 @@ module OpenapiFirst
|
|
|
65
65
|
|
|
66
66
|
return unless definition.key?('schema')
|
|
67
67
|
|
|
68
|
-
validation =
|
|
68
|
+
validation = JsonSchema.new(definition['schema'], openapi_version:)
|
|
69
69
|
value = unpacked_headers[name]
|
|
70
|
-
|
|
71
|
-
raise ResponseHeaderInvalidError,
|
|
70
|
+
schema_validation = validation.validate(value)
|
|
71
|
+
raise ResponseHeaderInvalidError, schema_validation.message if schema_validation.error?
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def unpack_response_headers(response_header_definitions, response_headers)
|
data/lib/openapi_first/router.rb
CHANGED
|
@@ -17,6 +17,7 @@ module OpenapiFirst
|
|
|
17
17
|
@app = app
|
|
18
18
|
@raise = options.fetch(:raise_error, false)
|
|
19
19
|
@not_found = options.fetch(:not_found, :halt)
|
|
20
|
+
@error_response_class = options.fetch(:error_response, Config.default_options.error_response)
|
|
20
21
|
spec = options.fetch(:spec)
|
|
21
22
|
raise "You have to pass spec: when initializing #{self.class}" unless spec
|
|
22
23
|
|
|
@@ -61,21 +62,13 @@ module OpenapiFirst
|
|
|
61
62
|
env[Rack::PATH_INFO] = Rack::Request.new(env).path
|
|
62
63
|
@router.call(env)
|
|
63
64
|
rescue BodyParsingError => e
|
|
64
|
-
|
|
65
|
-
ensure
|
|
66
|
-
env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def handle_body_parsing_error(_exception)
|
|
70
|
-
message = 'Failed to parse body as application/json'
|
|
65
|
+
message = e.message
|
|
71
66
|
raise RequestInvalidError, message if @raise
|
|
72
67
|
|
|
73
|
-
error =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
ErrorResponse::Default.new(**error).finish
|
|
68
|
+
error = RequestValidationError.new(status: 400, location: :body, message:)
|
|
69
|
+
@error_response_class.new(env, error).render
|
|
70
|
+
ensure
|
|
71
|
+
env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
|
|
79
72
|
end
|
|
80
73
|
|
|
81
74
|
def build_router(operations)
|
|
@@ -89,9 +82,8 @@ module OpenapiFirst
|
|
|
89
82
|
)
|
|
90
83
|
end
|
|
91
84
|
end
|
|
92
|
-
raise_error = @raise
|
|
93
85
|
Rack::Builder.app do
|
|
94
|
-
use(BodyParserMiddleware
|
|
86
|
+
use(BodyParserMiddleware)
|
|
95
87
|
run router
|
|
96
88
|
end
|
|
97
89
|
end
|
data/lib/openapi_first.rb
CHANGED
|
@@ -11,7 +11,8 @@ require_relative 'openapi_first/router'
|
|
|
11
11
|
require_relative 'openapi_first/request_validation'
|
|
12
12
|
require_relative 'openapi_first/response_validator'
|
|
13
13
|
require_relative 'openapi_first/response_validation'
|
|
14
|
-
require_relative 'openapi_first/
|
|
14
|
+
require_relative 'openapi_first/error_responses/default'
|
|
15
|
+
require_relative 'openapi_first/error_responses/json_api'
|
|
15
16
|
|
|
16
17
|
module OpenapiFirst
|
|
17
18
|
# The OpenAPI operation for the current request
|
|
@@ -35,18 +36,6 @@ module OpenapiFirst
|
|
|
35
36
|
# The parsed request body
|
|
36
37
|
REQUEST_BODY = 'openapi.parsed_request_body'
|
|
37
38
|
|
|
38
|
-
class << self
|
|
39
|
-
# Throws an error in the middle of the request validation to stop validation and send a response.
|
|
40
|
-
def error!(status, location = nil, title: nil, validation_result: nil)
|
|
41
|
-
throw :error, {
|
|
42
|
-
status:,
|
|
43
|
-
location:,
|
|
44
|
-
title: title || validation_result&.output&.fetch('error') || Rack::Utils::HTTP_STATUS_CODES[status],
|
|
45
|
-
validation_result:
|
|
46
|
-
}
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
39
|
def self.load(spec_path, only: nil)
|
|
51
40
|
resolved = Dir.chdir(File.dirname(spec_path)) do
|
|
52
41
|
content = YAML.load_file(File.basename(spec_path))
|
data/openapi_first.gemspec
CHANGED
|
@@ -37,8 +37,8 @@ Gem::Specification.new do |spec|
|
|
|
37
37
|
spec.add_runtime_dependency 'hanami-router', '~> 2.0.0'
|
|
38
38
|
spec.add_runtime_dependency 'json_refs', '~> 0.1', '>= 0.1.7'
|
|
39
39
|
spec.add_runtime_dependency 'json_schemer', '~> 2.0.0'
|
|
40
|
-
spec.add_runtime_dependency 'multi_json', '~> 1.
|
|
41
|
-
spec.add_runtime_dependency 'openapi_parameters', '
|
|
40
|
+
spec.add_runtime_dependency 'multi_json', '~> 1.15'
|
|
41
|
+
spec.add_runtime_dependency 'openapi_parameters', '>= 0.3.1', '< 2.0'
|
|
42
42
|
spec.add_runtime_dependency 'rack', '>= 2.2', '< 4.0'
|
|
43
43
|
spec.metadata = {
|
|
44
44
|
'rubygems_mfa_required' => 'true'
|
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: 1.0.0.
|
|
4
|
+
version: 1.0.0.beta5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andreas Haller
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-
|
|
11
|
+
date: 2023-11-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: hanami-router
|
|
@@ -64,28 +64,34 @@ dependencies:
|
|
|
64
64
|
requirements:
|
|
65
65
|
- - "~>"
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: '1.
|
|
67
|
+
version: '1.15'
|
|
68
68
|
type: :runtime
|
|
69
69
|
prerelease: false
|
|
70
70
|
version_requirements: !ruby/object:Gem::Requirement
|
|
71
71
|
requirements:
|
|
72
72
|
- - "~>"
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '1.
|
|
74
|
+
version: '1.15'
|
|
75
75
|
- !ruby/object:Gem::Dependency
|
|
76
76
|
name: openapi_parameters
|
|
77
77
|
requirement: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements:
|
|
79
|
-
- - "
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 0.3.1
|
|
82
|
+
- - "<"
|
|
80
83
|
- !ruby/object:Gem::Version
|
|
81
|
-
version:
|
|
84
|
+
version: '2.0'
|
|
82
85
|
type: :runtime
|
|
83
86
|
prerelease: false
|
|
84
87
|
version_requirements: !ruby/object:Gem::Requirement
|
|
85
88
|
requirements:
|
|
86
|
-
- - "
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: 0.3.1
|
|
92
|
+
- - "<"
|
|
87
93
|
- !ruby/object:Gem::Version
|
|
88
|
-
version:
|
|
94
|
+
version: '2.0'
|
|
89
95
|
- !ruby/object:Gem::Dependency
|
|
90
96
|
name: rack
|
|
91
97
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -153,22 +159,23 @@ files:
|
|
|
153
159
|
- lib/openapi_first.rb
|
|
154
160
|
- lib/openapi_first/body_parser_middleware.rb
|
|
155
161
|
- lib/openapi_first/config.rb
|
|
156
|
-
- lib/openapi_first/default_error_response.rb
|
|
157
162
|
- lib/openapi_first/definition.rb
|
|
158
163
|
- lib/openapi_first/error_response.rb
|
|
164
|
+
- lib/openapi_first/error_responses/default.rb
|
|
165
|
+
- lib/openapi_first/error_responses/json_api.rb
|
|
159
166
|
- lib/openapi_first/errors.rb
|
|
167
|
+
- lib/openapi_first/json_schema.rb
|
|
168
|
+
- lib/openapi_first/json_schema/result.rb
|
|
160
169
|
- lib/openapi_first/operation.rb
|
|
161
|
-
- lib/openapi_first/operation_schemas.rb
|
|
162
170
|
- lib/openapi_first/plugins.rb
|
|
163
171
|
- lib/openapi_first/request_body_validator.rb
|
|
164
172
|
- lib/openapi_first/request_validation.rb
|
|
173
|
+
- lib/openapi_first/request_validation_error.rb
|
|
165
174
|
- lib/openapi_first/response_validation.rb
|
|
166
175
|
- lib/openapi_first/response_validator.rb
|
|
167
176
|
- lib/openapi_first/router.rb
|
|
168
|
-
- lib/openapi_first/schema_validation.rb
|
|
169
177
|
- lib/openapi_first/string_keyed_hash.rb
|
|
170
178
|
- lib/openapi_first/use_router.rb
|
|
171
|
-
- lib/openapi_first/validation_result.rb
|
|
172
179
|
- lib/openapi_first/version.rb
|
|
173
180
|
- openapi_first.gemspec
|
|
174
181
|
homepage: https://github.com/ahx/openapi_first
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module OpenapiFirst
|
|
4
|
-
class DefaultErrorResponse < ErrorResponse
|
|
5
|
-
OpenapiFirst::Plugins.register_error_response(:default, self)
|
|
6
|
-
|
|
7
|
-
def body
|
|
8
|
-
MultiJson.dump({ errors: serialized_errors })
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def serialized_errors
|
|
12
|
-
return default_errors unless validation_output
|
|
13
|
-
|
|
14
|
-
key = pointer_key
|
|
15
|
-
[
|
|
16
|
-
{
|
|
17
|
-
source: { key => pointer(validation_output['instanceLocation']) },
|
|
18
|
-
title: validation_output['error']
|
|
19
|
-
}
|
|
20
|
-
]
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def default_errors
|
|
24
|
-
[{
|
|
25
|
-
status: status.to_s,
|
|
26
|
-
title:
|
|
27
|
-
}]
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def pointer_key
|
|
31
|
-
case location
|
|
32
|
-
when :request_body
|
|
33
|
-
:pointer
|
|
34
|
-
when :query, :path
|
|
35
|
-
:parameter
|
|
36
|
-
else
|
|
37
|
-
location
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def pointer(data_pointer)
|
|
42
|
-
return data_pointer if location == :request_body
|
|
43
|
-
|
|
44
|
-
data_pointer.delete_prefix('/')
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'openapi_parameters/parameter'
|
|
4
|
-
require_relative 'schema_validation'
|
|
5
|
-
|
|
6
|
-
module OpenapiFirst
|
|
7
|
-
# This class is basically a cache for JSON Schemas of parameters
|
|
8
|
-
class OperationSchemas
|
|
9
|
-
# @operation [OpenapiFirst::Operation]
|
|
10
|
-
def initialize(operation)
|
|
11
|
-
@operation = operation
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
attr_reader :operation
|
|
15
|
-
|
|
16
|
-
# Return JSON Schema of for all query parameters
|
|
17
|
-
def query_parameters_schema
|
|
18
|
-
@query_parameters_schema ||= build_json_schema(operation.query_parameters)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Return JSON Schema of for all path parameters
|
|
22
|
-
def path_parameters_schema
|
|
23
|
-
@path_parameters_schema ||= build_json_schema(operation.path_parameters)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def header_parameters_schema
|
|
27
|
-
@header_parameters_schema ||= build_json_schema(operation.header_parameters)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def cookie_parameters_schema
|
|
31
|
-
@cookie_parameters_schema ||= build_json_schema(operation.cookie_parameters)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
# Build JSON Schema for given parameter definitions
|
|
37
|
-
# @parameter_defs [Array<Hash>] Parameter definitions
|
|
38
|
-
def build_json_schema(parameter_defs)
|
|
39
|
-
init_schema = {
|
|
40
|
-
'type' => 'object',
|
|
41
|
-
'properties' => {},
|
|
42
|
-
'required' => []
|
|
43
|
-
}
|
|
44
|
-
schema = parameter_defs.each_with_object(init_schema) do |parameter_def, result|
|
|
45
|
-
parameter = OpenapiParameters::Parameter.new(parameter_def)
|
|
46
|
-
result['properties'][parameter.name] = parameter.schema if parameter.schema
|
|
47
|
-
result['required'] << parameter.name if parameter.required?
|
|
48
|
-
end
|
|
49
|
-
SchemaValidation.new(schema, openapi_version: operation.openapi_version)
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module OpenapiFirst
|
|
4
|
-
ValidationResult = Struct.new(:output, :schema, :data, keyword_init: true) do
|
|
5
|
-
def valid? = output['valid']
|
|
6
|
-
def error? = !output['valid']
|
|
7
|
-
|
|
8
|
-
# Returns a message that is used in exception messages.
|
|
9
|
-
def message
|
|
10
|
-
return if valid?
|
|
11
|
-
|
|
12
|
-
(output['errors']&.map { |e| e['error'] }&.join('. ') || output['error'])&.concat('.')
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|