openapi_first 1.0.0.beta4 → 1.0.0.beta5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|