openapi_first 0.6.4 → 0.6.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/Gemfile.lock +2 -2
- data/README.md +13 -24
- data/benchmarks/Gemfile.lock +1 -1
- data/benchmarks/apps/openapi_first_request_validation_only.ru +1 -2
- data/lib/openapi_first.rb +1 -2
- data/lib/openapi_first/app.rb +1 -2
- data/lib/openapi_first/query_parameters.rb +24 -0
- data/lib/openapi_first/request_validation.rb +135 -0
- data/lib/openapi_first/version.rb +1 -1
- metadata +4 -6
- data/lib/openapi_first/error_response_method.rb +0 -20
- data/lib/openapi_first/query_parameter_schemas.rb +0 -29
- data/lib/openapi_first/query_parameter_validation.rb +0 -56
- data/lib/openapi_first/request_body_validation.rb +0 -93
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 035e8a92e6ae6b8817bc3660141c33807d2b813ea79ccdaf045056947b37ed2b
|
4
|
+
data.tar.gz: 7e776d7b8d66a5c36a2d70fc73043b648c1d8fb034563064b35963e95aa8732d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d196918a2091123c0ad50da1a631da01a2b4923117400dedf01047cb12094375595bde9297de6887cd188258f6b2cb1acdea6a5d2b6af2551077a5788fc9ee2
|
7
|
+
data.tar.gz: 8289e602befcfa2d7e8aef2adf0c4d3663d624b68d002b2d3fd8ceb9266b20213cfeeca8191f8c84b76ce8d2b3dfe9fefe911d3673ad5b25a3263d5ddd377b5f
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
openapi_first (0.6.
|
4
|
+
openapi_first (0.6.5)
|
5
5
|
json_schemer (~> 0.2)
|
6
6
|
multi_json (~> 1.13)
|
7
7
|
oas_parser (~> 0.19)
|
@@ -32,7 +32,7 @@ GEM
|
|
32
32
|
i18n (1.7.0)
|
33
33
|
concurrent-ruby (~> 1.0)
|
34
34
|
jaro_winkler (1.5.3)
|
35
|
-
json_schemer (0.2.
|
35
|
+
json_schemer (0.2.8)
|
36
36
|
ecma-re-validator (~> 0.2)
|
37
37
|
hana (~> 1.3)
|
38
38
|
regexp_parser (~> 1.5)
|
data/README.md
CHANGED
@@ -77,8 +77,7 @@ OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
|
|
77
77
|
|
78
78
|
OpenapiFirst offers Rack middlewares to auto-implement different aspects for request handling:
|
79
79
|
|
80
|
-
-
|
81
|
-
- Request body validation
|
80
|
+
- Request validation
|
82
81
|
- Mapping request to a function call
|
83
82
|
|
84
83
|
It starts by adding a router middleware:
|
@@ -109,40 +108,30 @@ content-type: "application/vnd.api+json"
|
|
109
108
|
]
|
110
109
|
}
|
111
110
|
```
|
112
|
-
|
113
|
-
## Query parameter validation
|
111
|
+
## Request validation
|
114
112
|
|
115
113
|
```ruby
|
116
|
-
|
114
|
+
# Add the middleware:
|
115
|
+
use OpenapiFirst::RequestValidation
|
117
116
|
```
|
118
117
|
|
119
|
-
|
118
|
+
## Query parameter validation
|
120
119
|
|
121
|
-
|
122
|
-
use OpenapiFirst::QueryParameterValidation,
|
123
|
-
allow_additional_parameters: true
|
124
|
-
```
|
120
|
+
By default OpenapiFirst does not allow additional query parameters and will respond with 400 if additional parameters are sent. You can allow additional parameters with `allow_allow_unknown_query_parameters: true`:
|
125
121
|
|
126
122
|
The middleware filteres all top-level query parameters and adds these to the Rack env: `env[OpenapiFirst::QUERY_PARAMS]`.
|
127
|
-
If you want to forbid
|
123
|
+
If you want to forbid _nested_ query parameters you will need to use [`additionalProperties: false`](https://json-schema.org/understanding-json-schema/reference/object.html#properties) in your query parameter JSON schema.
|
128
124
|
|
129
|
-
|
125
|
+
_OpenapiFirst always treats query parameters like [`style: deepObject`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#style-values), **but** it just works with nested objects (`filter[foo][bar]=baz`) (see [this discussion](https://github.com/OAI/OpenAPI-Specification/issues/1706))._
|
130
126
|
|
131
|
-
|
132
|
-
|
133
|
-
tbd.
|
127
|
+
### Request body validation
|
134
128
|
|
135
|
-
|
136
|
-
|
137
|
-
```ruby
|
138
|
-
# Add the middleware:
|
139
|
-
use OpenapiFirst::RequestBodyValidation
|
140
|
-
```
|
141
|
-
|
142
|
-
This will return a `415` if the requests content type does not match or `400` if the request body is invalid.
|
129
|
+
The middleware will return a `415` if the requests content type does not match or `400` if the request body is invalid.
|
143
130
|
This will add the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
|
144
131
|
|
145
|
-
|
132
|
+
### Header, Cookie, Path parameter validation
|
133
|
+
|
134
|
+
tbd.
|
146
135
|
|
147
136
|
## Mapping the request to a method call
|
148
137
|
|
data/benchmarks/Gemfile.lock
CHANGED
@@ -25,7 +25,6 @@ end
|
|
25
25
|
|
26
26
|
spec = OpenapiFirst.load(File.absolute_path('./openapi.yaml', __dir__))
|
27
27
|
use OpenapiFirst::Router, spec: spec
|
28
|
-
use OpenapiFirst::
|
29
|
-
use OpenapiFirst::RequestBodyValidation
|
28
|
+
use OpenapiFirst::RequestValidation
|
30
29
|
|
31
30
|
run app
|
data/lib/openapi_first.rb
CHANGED
@@ -5,8 +5,7 @@ require 'oas_parser'
|
|
5
5
|
require 'openapi_first/definition'
|
6
6
|
require 'openapi_first/version'
|
7
7
|
require 'openapi_first/router'
|
8
|
-
require 'openapi_first/
|
9
|
-
require 'openapi_first/request_body_validation'
|
8
|
+
require 'openapi_first/request_validation'
|
10
9
|
require 'openapi_first/response_validator'
|
11
10
|
require 'openapi_first/operation_resolver'
|
12
11
|
require 'openapi_first/app'
|
data/lib/openapi_first/app.rb
CHANGED
@@ -14,8 +14,7 @@ module OpenapiFirst
|
|
14
14
|
use OpenapiFirst::Router,
|
15
15
|
spec: spec,
|
16
16
|
allow_unknown_operation: allow_unknown_operation
|
17
|
-
use OpenapiFirst::
|
18
|
-
use OpenapiFirst::RequestBodyValidation
|
17
|
+
use OpenapiFirst::RequestValidation
|
19
18
|
run OpenapiFirst::OperationResolver.new(app, namespace: namespace)
|
20
19
|
end
|
21
20
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
class QueryParameters
|
5
|
+
def initialize(operation:, allow_unknown_parameters: false)
|
6
|
+
@operation = operation
|
7
|
+
@allow_unknown_parameters = allow_unknown_parameters
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_json_schema
|
11
|
+
return unless @operation&.query_parameters&.any?
|
12
|
+
|
13
|
+
@operation.query_parameters.each_with_object(
|
14
|
+
'type' => 'object',
|
15
|
+
'required' => [],
|
16
|
+
'additionalProperties' => @allow_unknown_parameters,
|
17
|
+
'properties' => {}
|
18
|
+
) do |parameter, schema|
|
19
|
+
schema['required'] << parameter.name if parameter.required
|
20
|
+
schema['properties'][parameter.name] = parameter.schema
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
require 'json_schemer'
|
5
|
+
require 'multi_json'
|
6
|
+
require_relative 'query_parameters'
|
7
|
+
require_relative 'validation_format'
|
8
|
+
|
9
|
+
module OpenapiFirst
|
10
|
+
class RequestValidation
|
11
|
+
def initialize(app, allow_unknown_query_parameters: false)
|
12
|
+
@app = app
|
13
|
+
@allow_unknown_query_parameters = allow_unknown_query_parameters
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
17
|
+
operation = env[OpenapiFirst::OPERATION]
|
18
|
+
return @app.call(env) unless operation
|
19
|
+
|
20
|
+
req = Rack::Request.new(env)
|
21
|
+
catch(:halt) do
|
22
|
+
validate_query_parameters!(env, operation, req.params)
|
23
|
+
content_type = req.content_type
|
24
|
+
return @app.call(env) unless operation.request_body
|
25
|
+
|
26
|
+
validate_request_content_type!(content_type, operation)
|
27
|
+
body = req.body.read
|
28
|
+
req.body.rewind
|
29
|
+
parse_and_validate_request_body!(env, content_type, body, operation)
|
30
|
+
@app.call(env)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def halt(response)
|
35
|
+
throw :halt, response
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_and_validate_request_body!(env, content_type, body, operation)
|
39
|
+
validate_request_body_presence!(body, operation)
|
40
|
+
return if body.empty?
|
41
|
+
|
42
|
+
schema = request_body_schema(content_type, operation)
|
43
|
+
return unless schema
|
44
|
+
|
45
|
+
parsed_request_body = MultiJson.load(body)
|
46
|
+
errors = validate_json_schema(schema, parsed_request_body)
|
47
|
+
if errors.any?
|
48
|
+
halt(error_response(400, serialize_request_body_errors(errors)))
|
49
|
+
end
|
50
|
+
env[OpenapiFirst::REQUEST_BODY] = parsed_request_body
|
51
|
+
end
|
52
|
+
|
53
|
+
def validate_request_content_type!(content_type, operation)
|
54
|
+
return if operation.request_body.content[content_type]
|
55
|
+
|
56
|
+
halt(error_response(415))
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate_request_body_presence!(body, operation)
|
60
|
+
return unless operation.request_body.required && body.empty?
|
61
|
+
|
62
|
+
halt(error_response(415, 'Request body is required'))
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_json_schema(schema, object)
|
66
|
+
JSONSchemer.schema(schema).validate(object)
|
67
|
+
end
|
68
|
+
|
69
|
+
def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
|
70
|
+
{
|
71
|
+
status: status.to_s,
|
72
|
+
title: title
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def error_response(status, errors = [default_error(status)])
|
77
|
+
Rack::Response.new(
|
78
|
+
MultiJson.dump(errors: errors),
|
79
|
+
status,
|
80
|
+
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
81
|
+
).finish
|
82
|
+
end
|
83
|
+
|
84
|
+
def request_body_schema(content_type, endpoint)
|
85
|
+
return unless endpoint
|
86
|
+
|
87
|
+
endpoint.request_body.content[content_type]&.fetch('schema')
|
88
|
+
end
|
89
|
+
|
90
|
+
def serialize_request_body_errors(validation_errors)
|
91
|
+
validation_errors.map do |error|
|
92
|
+
{
|
93
|
+
source: {
|
94
|
+
pointer: error['data_pointer']
|
95
|
+
}
|
96
|
+
}.update(ValidationFormat.error_details(error))
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def validate_query_parameters!(env, operation, params)
|
101
|
+
json_schema = QueryParameters.new(
|
102
|
+
operation: operation,
|
103
|
+
allow_unknown_parameters: @allow_unknown_query_parameters
|
104
|
+
).to_json_schema
|
105
|
+
|
106
|
+
return unless json_schema
|
107
|
+
|
108
|
+
errors = JSONSchemer.schema(json_schema).validate(params)
|
109
|
+
if errors.any?
|
110
|
+
halt error_response(400, serialize_query_parameter_errors(errors))
|
111
|
+
end
|
112
|
+
env[QUERY_PARAMS] = allowed_params(json_schema, params)
|
113
|
+
end
|
114
|
+
|
115
|
+
def allowed_params(json_schema, params)
|
116
|
+
json_schema['properties']
|
117
|
+
.keys
|
118
|
+
.each_with_object({}) do |parameter_name, filtered|
|
119
|
+
next unless params.key?(parameter_name)
|
120
|
+
|
121
|
+
filtered[parameter_name] = params[parameter_name]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def serialize_query_parameter_errors(validation_errors)
|
126
|
+
validation_errors.map do |error|
|
127
|
+
{
|
128
|
+
source: {
|
129
|
+
parameter: File.basename(error['data_pointer'])
|
130
|
+
}
|
131
|
+
}.update(ValidationFormat.error_details(error))
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: openapi_first
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andreas Haller
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-10-
|
11
|
+
date: 2019-10-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json_schemer
|
@@ -161,11 +161,9 @@ files:
|
|
161
161
|
- lib/openapi_first/app.rb
|
162
162
|
- lib/openapi_first/coverage.rb
|
163
163
|
- lib/openapi_first/definition.rb
|
164
|
-
- lib/openapi_first/error_response_method.rb
|
165
164
|
- lib/openapi_first/operation_resolver.rb
|
166
|
-
- lib/openapi_first/
|
167
|
-
- lib/openapi_first/
|
168
|
-
- lib/openapi_first/request_body_validation.rb
|
165
|
+
- lib/openapi_first/query_parameters.rb
|
166
|
+
- lib/openapi_first/request_validation.rb
|
169
167
|
- lib/openapi_first/response_validator.rb
|
170
168
|
- lib/openapi_first/router.rb
|
171
169
|
- lib/openapi_first/validation.rb
|
@@ -1,20 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module OpenapiFirst
|
4
|
-
module ErrorResponseMethod
|
5
|
-
def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
|
6
|
-
{
|
7
|
-
status: status.to_s,
|
8
|
-
title: title
|
9
|
-
}
|
10
|
-
end
|
11
|
-
|
12
|
-
def error_response(status, errors = [default_error(status)])
|
13
|
-
Rack::Response.new(
|
14
|
-
MultiJson.dump(errors: errors),
|
15
|
-
status,
|
16
|
-
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
17
|
-
).finish
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
@@ -1,29 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module OpenapiFirst
|
4
|
-
class QueryParameterSchemas
|
5
|
-
def initialize(allow_additional_parameters:)
|
6
|
-
@additional_properties = allow_additional_parameters
|
7
|
-
end
|
8
|
-
|
9
|
-
def find(operation)
|
10
|
-
build_parameter_schema(operation)
|
11
|
-
end
|
12
|
-
|
13
|
-
private
|
14
|
-
|
15
|
-
def build_parameter_schema(operation)
|
16
|
-
return unless operation&.query_parameters&.any?
|
17
|
-
|
18
|
-
operation.query_parameters.each_with_object(
|
19
|
-
'type' => 'object',
|
20
|
-
'required' => [],
|
21
|
-
'additionalProperties' => @additional_properties,
|
22
|
-
'properties' => {}
|
23
|
-
) do |parameter, schema|
|
24
|
-
schema['required'] << parameter.name if parameter.required
|
25
|
-
schema['properties'][parameter.name] = parameter.schema
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
@@ -1,56 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rack'
|
4
|
-
require 'json_schemer'
|
5
|
-
require 'multi_json'
|
6
|
-
require_relative 'validation_format'
|
7
|
-
require_relative 'error_response_method'
|
8
|
-
require_relative 'query_parameter_schemas'
|
9
|
-
|
10
|
-
module OpenapiFirst
|
11
|
-
class QueryParameterValidation
|
12
|
-
include ErrorResponseMethod
|
13
|
-
|
14
|
-
def initialize(app, allow_additional_parameters: false)
|
15
|
-
@app = app
|
16
|
-
@schemas = QueryParameterSchemas.new(
|
17
|
-
allow_additional_parameters: allow_additional_parameters
|
18
|
-
)
|
19
|
-
@additional_properties = allow_additional_parameters
|
20
|
-
end
|
21
|
-
|
22
|
-
def call(env)
|
23
|
-
req = Rack::Request.new(env)
|
24
|
-
operation = env[OpenapiFirst::OPERATION]
|
25
|
-
schema = operation && @schemas.find(operation)
|
26
|
-
if schema
|
27
|
-
params = req.params
|
28
|
-
errors = schema && JSONSchemer.schema(schema).validate(params)
|
29
|
-
return error_response(400, serialize_errors(errors)) if errors&.any?
|
30
|
-
|
31
|
-
req.env[QUERY_PARAMS] = allowed_query_parameters(schema, params)
|
32
|
-
end
|
33
|
-
|
34
|
-
@app.call(env)
|
35
|
-
end
|
36
|
-
|
37
|
-
def allowed_query_parameters(params_schema, query_params)
|
38
|
-
params_schema['properties']
|
39
|
-
.keys
|
40
|
-
.each_with_object({}) do |parameter_name, filtered|
|
41
|
-
value = query_params[parameter_name]
|
42
|
-
filtered[parameter_name] = value if value
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def serialize_errors(validation_errors)
|
47
|
-
validation_errors.map do |error|
|
48
|
-
{
|
49
|
-
source: {
|
50
|
-
parameter: File.basename(error['data_pointer'])
|
51
|
-
}
|
52
|
-
}.update(ValidationFormat.error_details(error))
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
@@ -1,93 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rack'
|
4
|
-
require 'json_schemer'
|
5
|
-
require 'multi_json'
|
6
|
-
require_relative 'error_response_method'
|
7
|
-
require_relative 'validation_format'
|
8
|
-
|
9
|
-
module OpenapiFirst
|
10
|
-
class RequestBodyValidation
|
11
|
-
include ErrorResponseMethod
|
12
|
-
|
13
|
-
def initialize(app)
|
14
|
-
@app = app
|
15
|
-
end
|
16
|
-
|
17
|
-
def call(env) # rubocop:disable Metrics/MethodLength
|
18
|
-
operation = env[OpenapiFirst::OPERATION]
|
19
|
-
return @app.call(env) unless operation&.request_body
|
20
|
-
|
21
|
-
req = Rack::Request.new(env)
|
22
|
-
body = req.body.read
|
23
|
-
req.body.rewind
|
24
|
-
content_type = req.content_type
|
25
|
-
catch(:halt) do
|
26
|
-
validate_request_content_type!(content_type, operation)
|
27
|
-
validate_request_body_presence!(env, body, operation)
|
28
|
-
parse_and_validate_request_body!(env, content_type, body, operation)
|
29
|
-
@app.call(env)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def halt(response)
|
34
|
-
throw :halt, response
|
35
|
-
end
|
36
|
-
|
37
|
-
def validate_request_content_type!(content_type, operation)
|
38
|
-
return if content_type_valid?(content_type, operation)
|
39
|
-
|
40
|
-
halt(error_response(415))
|
41
|
-
end
|
42
|
-
|
43
|
-
def validate_request_body_presence!(env, body, operation)
|
44
|
-
return unless body.size.zero?
|
45
|
-
|
46
|
-
if operation.request_body.required
|
47
|
-
halt(error_response(415, 'Request body is required'))
|
48
|
-
end
|
49
|
-
halt(@app.call(env))
|
50
|
-
end
|
51
|
-
|
52
|
-
def parse_and_validate_request_body!(env, content_type, body, operation)
|
53
|
-
schema = request_body_schema(content_type, operation)
|
54
|
-
return unless schema
|
55
|
-
|
56
|
-
parsed_request_body = MultiJson.load(body)
|
57
|
-
errors = validate_json_schema(schema, parsed_request_body)
|
58
|
-
halt(error_response(400, serialize_errors(errors))) if errors&.any?
|
59
|
-
env[OpenapiFirst::REQUEST_BODY] = parsed_request_body
|
60
|
-
end
|
61
|
-
|
62
|
-
def validate_json_schema(schema, object)
|
63
|
-
JSONSchemer.schema(schema).validate(object)
|
64
|
-
end
|
65
|
-
|
66
|
-
def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
|
67
|
-
{
|
68
|
-
status: status.to_s,
|
69
|
-
title: title
|
70
|
-
}
|
71
|
-
end
|
72
|
-
|
73
|
-
def content_type_valid?(content_type, endpoint)
|
74
|
-
endpoint.request_body.content[content_type]
|
75
|
-
end
|
76
|
-
|
77
|
-
def request_body_schema(content_type, endpoint)
|
78
|
-
return unless endpoint
|
79
|
-
|
80
|
-
endpoint.request_body.content[content_type]&.fetch('schema')
|
81
|
-
end
|
82
|
-
|
83
|
-
def serialize_errors(validation_errors)
|
84
|
-
validation_errors.map do |error|
|
85
|
-
{
|
86
|
-
source: {
|
87
|
-
pointer: error['data_pointer']
|
88
|
-
}
|
89
|
-
}.update(ValidationFormat.error_details(error))
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|