openapi_first 1.0.0.beta6 → 1.0.0
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 +16 -3
- data/Gemfile +1 -0
- data/Gemfile.lock +11 -10
- data/Gemfile.rack2.lock +99 -0
- data/README.md +99 -130
- data/lib/openapi_first/body_parser.rb +5 -4
- data/lib/openapi_first/configuration.rb +20 -0
- data/lib/openapi_first/definition/operation.rb +84 -71
- data/lib/openapi_first/definition/parameters.rb +1 -1
- data/lib/openapi_first/definition/path_item.rb +21 -12
- data/lib/openapi_first/definition/path_parameters.rb +2 -3
- data/lib/openapi_first/definition/request_body.rb +22 -11
- data/lib/openapi_first/definition/response.rb +19 -31
- data/lib/openapi_first/definition/responses.rb +83 -0
- data/lib/openapi_first/definition.rb +50 -17
- data/lib/openapi_first/error_response.rb +22 -29
- data/lib/openapi_first/errors.rb +2 -14
- data/lib/openapi_first/failure.rb +55 -0
- data/lib/openapi_first/middlewares/request_validation.rb +52 -0
- data/lib/openapi_first/middlewares/response_validation.rb +35 -0
- data/lib/openapi_first/plugins/default/error_response.rb +74 -0
- data/lib/openapi_first/plugins/default.rb +11 -0
- data/lib/openapi_first/plugins/jsonapi/error_response.rb +58 -0
- data/lib/openapi_first/plugins/jsonapi.rb +11 -0
- data/lib/openapi_first/plugins.rb +9 -7
- data/lib/openapi_first/request_validation/request_body_validator.rb +41 -0
- data/lib/openapi_first/request_validation/validator.rb +81 -0
- data/lib/openapi_first/response_validation/validator.rb +101 -0
- data/lib/openapi_first/runtime_request.rb +84 -0
- data/lib/openapi_first/runtime_response.rb +31 -0
- data/lib/openapi_first/schema/validation_error.rb +18 -0
- data/lib/openapi_first/schema/validation_result.rb +32 -0
- data/lib/openapi_first/{definition/schema.rb → schema.rb} +8 -4
- data/lib/openapi_first/version.rb +1 -1
- data/lib/openapi_first.rb +32 -28
- data/openapi_first.gemspec +3 -5
- metadata +28 -20
- data/lib/openapi_first/config.rb +0 -20
- data/lib/openapi_first/definition/has_content.rb +0 -37
- data/lib/openapi_first/definition/schema/result.rb +0 -17
- data/lib/openapi_first/error_responses/default.rb +0 -58
- data/lib/openapi_first/error_responses/json_api.rb +0 -58
- data/lib/openapi_first/request_body_validator.rb +0 -37
- data/lib/openapi_first/request_validation.rb +0 -122
- data/lib/openapi_first/request_validation_error.rb +0 -31
- data/lib/openapi_first/response_validation.rb +0 -113
- data/lib/openapi_first/response_validator.rb +0 -21
- data/lib/openapi_first/router.rb +0 -68
- data/lib/openapi_first/use_router.rb +0 -18
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
class Failure
|
5
|
+
FAILURE = :openapi_first_validation_failure
|
6
|
+
|
7
|
+
TYPES = {
|
8
|
+
not_found: [NotFoundError, 'Request path is not defined.'],
|
9
|
+
method_not_allowed: [RequestInvalidError, 'Request method is not defined.'],
|
10
|
+
unsupported_media_type: [RequestInvalidError, 'Request content type is not defined.'],
|
11
|
+
invalid_body: [RequestInvalidError, 'Request body invalid:'],
|
12
|
+
invalid_query: [RequestInvalidError, 'Query parameter is invalid:'],
|
13
|
+
invalid_header: [RequestInvalidError, 'Request header is invalid:'],
|
14
|
+
invalid_path: [RequestInvalidError, 'Path segment is invalid:'],
|
15
|
+
invalid_cookie: [RequestInvalidError, 'Cookie value is invalid:'],
|
16
|
+
response_not_found: [ResponseNotFoundError, 'Response is not defined.'],
|
17
|
+
invalid_response_body: [ResponseInvalidError, 'Response body is invalid:'],
|
18
|
+
invalid_response_header: [ResponseInvalidError, 'Response header is invalid:']
|
19
|
+
}.freeze
|
20
|
+
private_constant :TYPES
|
21
|
+
|
22
|
+
# @param error_type [Symbol] See Failure::TYPES.keys
|
23
|
+
# @param errors [Array<OpenapiFirst::Schema::ValidationResult>]
|
24
|
+
def self.fail!(error_type, message: nil, errors: nil)
|
25
|
+
throw FAILURE, new(
|
26
|
+
error_type,
|
27
|
+
message:,
|
28
|
+
errors:
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param type [Symbol] See TYPES.keys
|
33
|
+
# @param message [String] A generic error message
|
34
|
+
# @param errors [Array<OpenapiFirst::Schema::ValidationError>]
|
35
|
+
def initialize(error_type, message: nil, errors: nil)
|
36
|
+
unless TYPES.key?(error_type)
|
37
|
+
raise ArgumentError,
|
38
|
+
"error_type must be one of #{TYPES.keys} but was #{error_type.inspect}"
|
39
|
+
end
|
40
|
+
|
41
|
+
@error_type = error_type
|
42
|
+
@message = message
|
43
|
+
@errors = errors
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_reader :error_type, :message, :errors
|
47
|
+
|
48
|
+
# Raise an exception that fits the failure.
|
49
|
+
def raise!
|
50
|
+
exception, message_prefix = TYPES.fetch(error_type)
|
51
|
+
|
52
|
+
raise exception, "#{message_prefix} #{@message || errors&.map(&:error)&.join('. ')}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
module OpenapiFirst
|
5
|
+
module Middlewares
|
6
|
+
# A Rack middleware to validate requests against an OpenAPI API description
|
7
|
+
class RequestValidation
|
8
|
+
# @param app The parent Rack application
|
9
|
+
# @param options An optional Hash of configuration options to override defaults
|
10
|
+
# :raise_error A Boolean indicating whether to raise an error if validation fails.
|
11
|
+
# default: false
|
12
|
+
# :error_response The Class to use for error responses.
|
13
|
+
# default: OpenapiFirst::Plugins::Default::ErrorResponse (Config.default_options.error_response)
|
14
|
+
def initialize(app, options = {})
|
15
|
+
@app = app
|
16
|
+
@raise = options.fetch(:raise_error, OpenapiFirst.configuration.request_validation_raise_error)
|
17
|
+
@error_response_class = error_response(options[:error_response])
|
18
|
+
|
19
|
+
spec = options.fetch(:spec)
|
20
|
+
raise "You have to pass spec: when initializing #{self.class}" unless spec
|
21
|
+
|
22
|
+
@definition = spec.is_a?(Definition) ? spec : OpenapiFirst.load(spec)
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(env)
|
26
|
+
request = find_request(env)
|
27
|
+
return @app.call(env) unless request
|
28
|
+
|
29
|
+
failure = if @raise
|
30
|
+
request.validate!
|
31
|
+
else
|
32
|
+
request.validate
|
33
|
+
end
|
34
|
+
return @error_response_class.new(failure:).render if failure
|
35
|
+
|
36
|
+
@app.call(env)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def find_request(env)
|
42
|
+
env[REQUEST] ||= @definition.request(Rack::Request.new(env))
|
43
|
+
end
|
44
|
+
|
45
|
+
def error_response(mod)
|
46
|
+
return OpenapiFirst.plugin(mod)::ErrorResponse if mod.is_a?(Symbol)
|
47
|
+
|
48
|
+
mod || OpenapiFirst.configuration.request_validation_error_response
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
module OpenapiFirst
|
5
|
+
module Middlewares
|
6
|
+
# A Rack middleware to validate requests against an OpenAPI API description
|
7
|
+
class ResponseValidation
|
8
|
+
# @param app The parent Rack application
|
9
|
+
# @param options Hash
|
10
|
+
# :spec Path to the OpenAPI file or an instance of Definition
|
11
|
+
def initialize(app, options = {})
|
12
|
+
@app = app
|
13
|
+
|
14
|
+
spec = options.fetch(:spec)
|
15
|
+
raise "You have to pass spec: when initializing #{self.class}" unless spec
|
16
|
+
|
17
|
+
@definition = spec.is_a?(Definition) ? spec : OpenapiFirst.load(spec)
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(env)
|
21
|
+
request = find_request(env)
|
22
|
+
response = @app.call(env)
|
23
|
+
request.response(response).validate!
|
24
|
+
|
25
|
+
response
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def find_request(env)
|
31
|
+
env[REQUEST] ||= @definition.request(Rack::Request.new(env))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
module Plugins
|
5
|
+
module Default
|
6
|
+
# An error reponse that returns application/problem+json with a list of "errors"
|
7
|
+
# See also https://www.rfc-editor.org/rfc/rfc9457.html
|
8
|
+
class ErrorResponse
|
9
|
+
include OpenapiFirst::ErrorResponse
|
10
|
+
|
11
|
+
TITLES = {
|
12
|
+
not_found: 'Not Found',
|
13
|
+
method_not_allowed: 'Request Method Not Allowed',
|
14
|
+
unsupported_media_type: 'Unsupported Media Type',
|
15
|
+
invalid_body: 'Bad Request Body',
|
16
|
+
invalid_query: 'Bad Query Parameter',
|
17
|
+
invalid_header: 'Bad Request Header',
|
18
|
+
invalid_path: 'Bad Request Path',
|
19
|
+
invalid_cookie: 'Bod Request Cookie'
|
20
|
+
}.freeze
|
21
|
+
private_constant :TITLES
|
22
|
+
|
23
|
+
def body
|
24
|
+
result = {
|
25
|
+
title:,
|
26
|
+
status:
|
27
|
+
}
|
28
|
+
result[:errors] = errors if failure.errors
|
29
|
+
MultiJson.dump(result)
|
30
|
+
end
|
31
|
+
|
32
|
+
def error_type = failure.error_type
|
33
|
+
|
34
|
+
def title
|
35
|
+
TITLES.fetch(error_type)
|
36
|
+
end
|
37
|
+
|
38
|
+
def content_type
|
39
|
+
'application/problem+json'
|
40
|
+
end
|
41
|
+
|
42
|
+
def errors
|
43
|
+
key = pointer_key
|
44
|
+
failure.errors.map do |error|
|
45
|
+
{
|
46
|
+
message: error.error,
|
47
|
+
key => pointer(error.instance_location),
|
48
|
+
code: error.type
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def pointer_key
|
54
|
+
case error_type
|
55
|
+
when :invalid_body
|
56
|
+
:pointer
|
57
|
+
when :invalid_query, :invalid_path
|
58
|
+
:parameter
|
59
|
+
when :invalid_header
|
60
|
+
:header
|
61
|
+
when :invalid_cookie
|
62
|
+
:cookie
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def pointer(data_pointer)
|
67
|
+
return data_pointer if error_type == :invalid_body
|
68
|
+
|
69
|
+
data_pointer.delete_prefix('/')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
module Plugins
|
5
|
+
module Jsonapi
|
6
|
+
class ErrorResponse
|
7
|
+
include OpenapiFirst::ErrorResponse
|
8
|
+
|
9
|
+
def body
|
10
|
+
MultiJson.dump({ errors: serialized_errors })
|
11
|
+
end
|
12
|
+
|
13
|
+
def content_type
|
14
|
+
'application/vnd.api+json'
|
15
|
+
end
|
16
|
+
|
17
|
+
def serialized_errors
|
18
|
+
return default_errors unless failure.errors
|
19
|
+
|
20
|
+
key = pointer_key
|
21
|
+
failure.errors.map do |error|
|
22
|
+
{
|
23
|
+
status: status.to_s,
|
24
|
+
source: { key => pointer(error.instance_location) },
|
25
|
+
title: error.error
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_errors
|
31
|
+
[{
|
32
|
+
status: status.to_s,
|
33
|
+
title: Rack::Utils::HTTP_STATUS_CODES[status]
|
34
|
+
}]
|
35
|
+
end
|
36
|
+
|
37
|
+
def pointer_key
|
38
|
+
case failure.error_type
|
39
|
+
when :invalid_body
|
40
|
+
:pointer
|
41
|
+
when :invalid_query, :invalid_path
|
42
|
+
:parameter
|
43
|
+
when :invalid_header
|
44
|
+
:header
|
45
|
+
when :invalid_cookie
|
46
|
+
:cookie
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def pointer(data_pointer)
|
51
|
+
return data_pointer if failure.error_type == :invalid_body
|
52
|
+
|
53
|
+
data_pointer.delete_prefix('/')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -1,17 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module OpenapiFirst
|
4
|
+
# Plugin System adapted from
|
5
|
+
# Polished Ruby Programming by Jeremy Evans
|
6
|
+
# https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewBook?id=0
|
4
7
|
module Plugins
|
5
|
-
|
8
|
+
PLUGINS = {} # rubocop:disable Style/MutableConstant
|
6
9
|
|
7
|
-
def
|
8
|
-
|
10
|
+
def register(name, klass)
|
11
|
+
PLUGINS[name] = klass
|
9
12
|
end
|
10
13
|
|
11
|
-
def
|
12
|
-
|
13
|
-
|
14
|
-
ERROR_RESPONSES.fetch(name)
|
14
|
+
def plugin(name)
|
15
|
+
require "openapi_first/plugins/#{name}"
|
16
|
+
PLUGINS.fetch(name)
|
15
17
|
end
|
16
18
|
end
|
17
19
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../failure'
|
4
|
+
|
5
|
+
module OpenapiFirst
|
6
|
+
module RequestValidation
|
7
|
+
class RequestBodyValidator
|
8
|
+
def initialize(operation)
|
9
|
+
@operation = operation
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate!(parsed_request_body, request_content_type)
|
13
|
+
request_body = operation.request_body
|
14
|
+
schema = request_body.schema_for(request_content_type)
|
15
|
+
unless schema
|
16
|
+
Failure.fail!(:unsupported_media_type,
|
17
|
+
message: "Unsupported Media Type '#{request_content_type}'")
|
18
|
+
end
|
19
|
+
|
20
|
+
if request_body.required? && parsed_request_body.nil?
|
21
|
+
Failure.fail!(:invalid_body,
|
22
|
+
message: 'Request body is not defined')
|
23
|
+
end
|
24
|
+
|
25
|
+
validate_body!(parsed_request_body, schema)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :operation
|
31
|
+
|
32
|
+
def validate_body!(parsed_request_body, schema)
|
33
|
+
request_body_schema = schema
|
34
|
+
return unless request_body_schema
|
35
|
+
|
36
|
+
validation = request_body_schema.validate(parsed_request_body)
|
37
|
+
Failure.fail!(:invalid_body, errors: validation.errors) if validation.error?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../failure'
|
4
|
+
require_relative 'request_body_validator'
|
5
|
+
|
6
|
+
module OpenapiFirst
|
7
|
+
module RequestValidation
|
8
|
+
class Validator
|
9
|
+
def initialize(operation)
|
10
|
+
@operation = operation
|
11
|
+
end
|
12
|
+
|
13
|
+
def validate(runtime_request)
|
14
|
+
catch Failure::FAILURE do
|
15
|
+
validate_defined(runtime_request)
|
16
|
+
validate_parameters!(runtime_request)
|
17
|
+
validate_request_body!(runtime_request)
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :operation, :raw_path_params
|
25
|
+
|
26
|
+
def validate_defined(request)
|
27
|
+
return if request.known?
|
28
|
+
return Failure.fail!(:not_found) unless request.known_path?
|
29
|
+
|
30
|
+
Failure.fail!(:method_not_allowed) unless request.known_request_method?
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_parameters!(request)
|
34
|
+
validate_query_params!(request)
|
35
|
+
validate_path_params!(request)
|
36
|
+
validate_cookie_params!(request)
|
37
|
+
validate_header_params!(request)
|
38
|
+
end
|
39
|
+
|
40
|
+
def validate_path_params!(request)
|
41
|
+
parameters = operation.path_parameters
|
42
|
+
return unless parameters
|
43
|
+
|
44
|
+
validation = parameters.schema.validate(request.path_parameters)
|
45
|
+
Failure.fail!(:invalid_path, errors: validation.errors) if validation.error?
|
46
|
+
end
|
47
|
+
|
48
|
+
def validate_query_params!(request)
|
49
|
+
parameters = operation.query_parameters
|
50
|
+
return unless parameters
|
51
|
+
|
52
|
+
validation = parameters.schema.validate(request.query)
|
53
|
+
Failure.fail!(:invalid_query, errors: validation.errors) if validation.error?
|
54
|
+
end
|
55
|
+
|
56
|
+
def validate_cookie_params!(request)
|
57
|
+
parameters = operation.cookie_parameters
|
58
|
+
return unless parameters
|
59
|
+
|
60
|
+
validation = parameters.schema.validate(request.cookies)
|
61
|
+
Failure.fail!(:invalid_cookie, errors: validation.errors) if validation.error?
|
62
|
+
end
|
63
|
+
|
64
|
+
def validate_header_params!(request)
|
65
|
+
parameters = operation.header_parameters
|
66
|
+
return unless parameters
|
67
|
+
|
68
|
+
validation = parameters.schema.validate(request.headers)
|
69
|
+
Failure.fail!(:invalid_header, errors: validation.errors) if validation.error?
|
70
|
+
end
|
71
|
+
|
72
|
+
def validate_request_body!(request)
|
73
|
+
return unless operation.request_body
|
74
|
+
|
75
|
+
RequestBodyValidator.new(operation).validate!(request.body, request.content_type)
|
76
|
+
rescue BodyParser::ParsingError => e
|
77
|
+
Failure.fail!(:invalid_body, message: e.message)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../failure'
|
4
|
+
|
5
|
+
module OpenapiFirst
|
6
|
+
module ResponseValidation
|
7
|
+
class Validator
|
8
|
+
def initialize(operation)
|
9
|
+
@operation = operation
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate(rack_response)
|
13
|
+
return unless operation
|
14
|
+
|
15
|
+
response = Rack::Response[*rack_response.to_a]
|
16
|
+
catch Failure::FAILURE do
|
17
|
+
response_definition = response_for(operation, response.status, response.content_type)
|
18
|
+
validate_response_body(response_definition.content_schema, response.body)
|
19
|
+
validate_response_headers(response_definition.headers, response.headers)
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :operation
|
27
|
+
|
28
|
+
def response_for(operation, status, content_type)
|
29
|
+
response = operation.response_for(status, content_type)
|
30
|
+
return response if response
|
31
|
+
|
32
|
+
unless operation.response_status_defined?(status)
|
33
|
+
message = "Response status '#{status}' not found for '#{operation.name}'"
|
34
|
+
Failure.fail!(:response_not_found, message:)
|
35
|
+
end
|
36
|
+
if content_type.nil? || content_type.empty?
|
37
|
+
message = "Content-Type for '#{operation.name}' must not be empty"
|
38
|
+
Failure.fail!(:invalid_response_header, message:)
|
39
|
+
end
|
40
|
+
|
41
|
+
message = "Content-Type '#{content_type}' is not defined for '#{operation.name}'"
|
42
|
+
Failure.fail!(:invalid_response_header, message:)
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate_response_body(schema, response)
|
46
|
+
return unless schema
|
47
|
+
|
48
|
+
full_body = +''
|
49
|
+
response.each { |chunk| full_body << chunk }
|
50
|
+
data = full_body.empty? ? {} : load_json(full_body)
|
51
|
+
validation = schema.validate(data)
|
52
|
+
Failure.fail!(:invalid_response_body, errors: validation.errors) if validation.error?
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate_response_headers(response_header_definitions, response_headers)
|
56
|
+
return unless response_header_definitions
|
57
|
+
|
58
|
+
unpacked_headers = unpack_response_headers(response_header_definitions, response_headers)
|
59
|
+
response_header_definitions.each do |name, definition|
|
60
|
+
next if name == 'Content-Type'
|
61
|
+
|
62
|
+
validate_response_header(name, definition, unpacked_headers, openapi_version: operation.openapi_version)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def validate_response_header(name, definition, unpacked_headers, openapi_version:)
|
67
|
+
unless unpacked_headers.key?(name)
|
68
|
+
if definition['required']
|
69
|
+
Failure.fail!(:invalid_response_header,
|
70
|
+
message: "Required response header '#{name}' is missing")
|
71
|
+
end
|
72
|
+
|
73
|
+
return
|
74
|
+
end
|
75
|
+
|
76
|
+
return unless definition.key?('schema')
|
77
|
+
|
78
|
+
validation = Schema.new(definition['schema'], openapi_version:)
|
79
|
+
value = unpacked_headers[name]
|
80
|
+
validation_result = validation.validate(value)
|
81
|
+
return unless validation_result.error?
|
82
|
+
|
83
|
+
Failure.fail!(:invalid_response_header,
|
84
|
+
errors: validation_result.errors)
|
85
|
+
end
|
86
|
+
|
87
|
+
def unpack_response_headers(response_header_definitions, response_headers)
|
88
|
+
headers_as_parameters = response_header_definitions.map do |name, definition|
|
89
|
+
definition.merge('name' => name, 'in' => 'header')
|
90
|
+
end
|
91
|
+
OpenapiParameters::Header.new(headers_as_parameters).unpack(response_headers)
|
92
|
+
end
|
93
|
+
|
94
|
+
def load_json(string)
|
95
|
+
MultiJson.load(string)
|
96
|
+
rescue MultiJson::ParseError
|
97
|
+
string
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require_relative 'runtime_response'
|
5
|
+
require_relative 'body_parser'
|
6
|
+
require_relative 'request_validation/validator'
|
7
|
+
|
8
|
+
module OpenapiFirst
|
9
|
+
# RuntimeRequest represents how an incoming request (Rack::Request) matches a request definition.
|
10
|
+
class RuntimeRequest
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
def initialize(request:, path_item:, operation:, path_params:)
|
14
|
+
@request = request
|
15
|
+
@path_item = path_item
|
16
|
+
@operation = operation
|
17
|
+
@original_path_params = path_params
|
18
|
+
end
|
19
|
+
|
20
|
+
def_delegators :@request, :content_type, :media_type
|
21
|
+
def_delegators :@operation, :operation_id
|
22
|
+
|
23
|
+
def known?
|
24
|
+
known_path? && known_request_method?
|
25
|
+
end
|
26
|
+
|
27
|
+
def known_path?
|
28
|
+
!!path_item
|
29
|
+
end
|
30
|
+
|
31
|
+
def known_request_method?
|
32
|
+
!!operation
|
33
|
+
end
|
34
|
+
|
35
|
+
# Merged path and query parameters
|
36
|
+
def params
|
37
|
+
@params ||= query.merge(path_parameters)
|
38
|
+
end
|
39
|
+
|
40
|
+
def path_parameters
|
41
|
+
@path_parameters ||=
|
42
|
+
operation.path_parameters&.unpack(@original_path_params) || {}
|
43
|
+
end
|
44
|
+
|
45
|
+
def query
|
46
|
+
@query ||=
|
47
|
+
operation.query_parameters&.unpack(request.env) || {}
|
48
|
+
end
|
49
|
+
|
50
|
+
alias query_parameters query
|
51
|
+
|
52
|
+
def headers
|
53
|
+
@headers ||=
|
54
|
+
operation.header_parameters&.unpack(request.env) || {}
|
55
|
+
end
|
56
|
+
|
57
|
+
def cookies
|
58
|
+
@cookies ||=
|
59
|
+
operation.cookie_parameters&.unpack(request.env) || {}
|
60
|
+
end
|
61
|
+
|
62
|
+
def body
|
63
|
+
@body ||= BodyParser.new.parse(request, request.media_type)
|
64
|
+
end
|
65
|
+
alias parsed_body body
|
66
|
+
|
67
|
+
def validate
|
68
|
+
RequestValidation::Validator.new(operation).validate(self)
|
69
|
+
end
|
70
|
+
|
71
|
+
def validate!
|
72
|
+
error = validate
|
73
|
+
error&.raise!
|
74
|
+
end
|
75
|
+
|
76
|
+
def response(rack_response)
|
77
|
+
RuntimeResponse.new(operation, rack_response)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
attr_reader :request, :operation, :path_item
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'response_validation/validator'
|
4
|
+
|
5
|
+
module OpenapiFirst
|
6
|
+
class RuntimeResponse
|
7
|
+
def initialize(operation, rack_response)
|
8
|
+
@operation = operation
|
9
|
+
@rack_response = rack_response
|
10
|
+
end
|
11
|
+
|
12
|
+
def description
|
13
|
+
response_definition&.description
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate
|
17
|
+
ResponseValidation::Validator.new(@operation).validate(@rack_response)
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate!
|
21
|
+
error = validate
|
22
|
+
error&.raise!
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def response_definition
|
28
|
+
@response_definition ||= @operation.response_for(@rack_response.status, @rack_response.content_type)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
class Schema
|
5
|
+
class ValidationError
|
6
|
+
def initialize(json_schemer_error)
|
7
|
+
@error = json_schemer_error
|
8
|
+
end
|
9
|
+
|
10
|
+
def error = @error['error']
|
11
|
+
def schemer_error = @error
|
12
|
+
def instance_location = @error['data_pointer']
|
13
|
+
def schema_location = @error['schema_pointer']
|
14
|
+
def type = @error['type']
|
15
|
+
def details = @error['details']
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|