openapi_first 1.0.0.beta1 → 1.0.0.beta4
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/.github/workflows/ruby.yml +8 -20
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +23 -0
- data/Gemfile +4 -1
- data/Gemfile.lock +39 -51
- data/README.md +27 -25
- data/benchmarks/Gemfile.lock +28 -33
- data/benchmarks/apps/openapi_first_with_hanami_api.ru +1 -2
- data/benchmarks/apps/openapi_first_with_plain_rack.ru +32 -0
- data/benchmarks/apps/openapi_first_with_response_validation.ru +14 -11
- data/benchmarks/apps/openapi_first_with_sinatra.ru +29 -0
- data/lib/openapi_first/body_parser_middleware.rb +1 -1
- data/lib/openapi_first/config.rb +19 -0
- data/lib/openapi_first/default_error_response.rb +47 -0
- data/lib/openapi_first/definition.rb +8 -1
- data/lib/openapi_first/error_response.rb +31 -0
- data/lib/openapi_first/errors.rb +3 -40
- data/lib/openapi_first/operation.rb +33 -14
- data/lib/openapi_first/operation_schemas.rb +52 -0
- data/lib/openapi_first/plugins.rb +17 -0
- data/lib/openapi_first/request_body_validator.rb +41 -0
- data/lib/openapi_first/request_validation.rb +66 -84
- data/lib/openapi_first/response_validation.rb +38 -7
- data/lib/openapi_first/response_validator.rb +1 -1
- data/lib/openapi_first/router.rb +15 -14
- data/lib/openapi_first/schema_validation.rb +22 -21
- data/lib/openapi_first/string_keyed_hash.rb +20 -0
- data/lib/openapi_first/validation_result.rb +15 -0
- data/lib/openapi_first/version.rb +1 -1
- data/lib/openapi_first.rb +30 -21
- data/openapi_first.gemspec +4 -12
- metadata +20 -117
- data/benchmarks/apps/openapi_first.ru +0 -22
- data/lib/openapi_first/utils.rb +0 -35
- data/lib/openapi_first/validation_format.rb +0 -55
- /data/benchmarks/apps/{committee.ru → committee_with_hanami_api.ru} +0 -0
@@ -0,0 +1,47 @@
|
|
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
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require_relative 'operation'
|
4
4
|
|
5
5
|
module OpenapiFirst
|
6
|
+
# Represents an OpenAPI API Description document
|
6
7
|
class Definition
|
7
8
|
attr_reader :filepath, :operations
|
8
9
|
|
@@ -11,9 +12,15 @@ module OpenapiFirst
|
|
11
12
|
methods = %w[get head post put patch delete trace options]
|
12
13
|
@operations = resolved['paths'].flat_map do |path, path_item|
|
13
14
|
path_item.slice(*methods).map do |request_method, _operation_object|
|
14
|
-
Operation.new(path, request_method, path_item)
|
15
|
+
Operation.new(path, request_method, path_item, openapi_version: detect_version(resolved))
|
15
16
|
end
|
16
17
|
end
|
17
18
|
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def detect_version(resolved)
|
23
|
+
(resolved['openapi'] || resolved['swagger'])[0..2]
|
24
|
+
end
|
18
25
|
end
|
19
26
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
# This is the base class for error responses
|
5
|
+
class ErrorResponse
|
6
|
+
## @param status [Integer] The HTTP status code.
|
7
|
+
## @param title [String] The title of the error. Usually the name of the HTTP status code.
|
8
|
+
## @param location [Symbol] The location of the error (:request_body, :query, :header, :cookie, :path).
|
9
|
+
## @param validation_result [ValidationResult]
|
10
|
+
def initialize(status:, location:, title:, validation_result:)
|
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
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :status, :location, :title, :schema, :data, :validation_output
|
20
|
+
|
21
|
+
def render
|
22
|
+
Rack::Response.new(body, status, Rack::CONTENT_TYPE => content_type).finish
|
23
|
+
end
|
24
|
+
|
25
|
+
def content_type = 'application/json'
|
26
|
+
|
27
|
+
def body
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/openapi_first/errors.rb
CHANGED
@@ -5,15 +5,6 @@ module OpenapiFirst
|
|
5
5
|
|
6
6
|
class NotFoundError < Error; end
|
7
7
|
|
8
|
-
class HandlerNotFoundError < Error; end
|
9
|
-
|
10
|
-
class NotImplementedError < Error
|
11
|
-
def initialize(message)
|
12
|
-
warn 'NotImplementedError is deprecated. Handle HandlerNotFoundError instead'
|
13
|
-
super
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
8
|
class ResponseInvalid < Error; end
|
18
9
|
|
19
10
|
class ResponseCodeNotFoundError < ResponseInvalid; end
|
@@ -22,37 +13,9 @@ module OpenapiFirst
|
|
22
13
|
|
23
14
|
class ResponseBodyInvalidError < ResponseInvalid; end
|
24
15
|
|
16
|
+
class ResponseHeaderInvalidError < ResponseInvalid; end
|
17
|
+
|
25
18
|
class BodyParsingError < Error; end
|
26
19
|
|
27
|
-
class RequestInvalidError < Error
|
28
|
-
def initialize(serialized_errors)
|
29
|
-
message = error_message(serialized_errors)
|
30
|
-
super message
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
|
35
|
-
def error_message(errors)
|
36
|
-
errors.map do |error|
|
37
|
-
[human_source(error), human_error(error)].compact.join(' ')
|
38
|
-
end.join(', ')
|
39
|
-
end
|
40
|
-
|
41
|
-
def human_source(error)
|
42
|
-
return unless error[:source]
|
43
|
-
|
44
|
-
source_key = error[:source].keys.first
|
45
|
-
source = {
|
46
|
-
pointer: 'Request body invalid:',
|
47
|
-
parameter: 'Query parameter invalid:'
|
48
|
-
}.fetch(source_key, source_key)
|
49
|
-
name = error[:source].values.first
|
50
|
-
source += " #{name}" unless name.nil? || name.empty?
|
51
|
-
source
|
52
|
-
end
|
53
|
-
|
54
|
-
def human_error(error)
|
55
|
-
error[:title]
|
56
|
-
end
|
57
|
-
end
|
20
|
+
class RequestInvalidError < Error; end
|
58
21
|
end
|
@@ -3,10 +3,10 @@
|
|
3
3
|
require 'forwardable'
|
4
4
|
require 'set'
|
5
5
|
require_relative 'schema_validation'
|
6
|
-
require_relative '
|
6
|
+
require_relative 'operation_schemas'
|
7
7
|
|
8
8
|
module OpenapiFirst
|
9
|
-
class Operation
|
9
|
+
class Operation # rubocop:disable Metrics/ClassLength
|
10
10
|
extend Forwardable
|
11
11
|
def_delegators :operation_object,
|
12
12
|
:[],
|
@@ -15,12 +15,13 @@ module OpenapiFirst
|
|
15
15
|
WRITE_METHODS = Set.new(%w[post put patch delete]).freeze
|
16
16
|
private_constant :WRITE_METHODS
|
17
17
|
|
18
|
-
attr_reader :path, :method
|
18
|
+
attr_reader :path, :method, :openapi_version
|
19
19
|
|
20
|
-
def initialize(path, request_method, path_item_object)
|
20
|
+
def initialize(path, request_method, path_item_object, openapi_version:)
|
21
21
|
@path = path
|
22
22
|
@method = request_method
|
23
23
|
@path_item_object = path_item_object
|
24
|
+
@openapi_version = openapi_version
|
24
25
|
end
|
25
26
|
|
26
27
|
def operation_id
|
@@ -39,7 +40,7 @@ module OpenapiFirst
|
|
39
40
|
operation_object['requestBody']
|
40
41
|
end
|
41
42
|
|
42
|
-
def
|
43
|
+
def response_body_schema(status, content_type)
|
43
44
|
content = response_for(status)['content']
|
44
45
|
return if content.nil? || content.empty?
|
45
46
|
|
@@ -52,16 +53,18 @@ module OpenapiFirst
|
|
52
53
|
raise ResponseContentTypeNotFoundError, message
|
53
54
|
end
|
54
55
|
schema = media_type['schema']
|
55
|
-
|
56
|
+
return unless schema
|
57
|
+
|
58
|
+
SchemaValidation.new(schema, write: false, openapi_version:)
|
56
59
|
end
|
57
60
|
|
58
61
|
def request_body_schema(request_content_type)
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
62
|
+
(@request_body_schema ||= {})[request_content_type] ||= begin
|
63
|
+
content = operation_object.dig('requestBody', 'content')
|
64
|
+
media_type = find_content_for_content_type(content, request_content_type)
|
65
|
+
schema = media_type&.fetch('schema', nil)
|
66
|
+
SchemaValidation.new(schema, write: write?, openapi_version:) if schema
|
67
|
+
end
|
65
68
|
end
|
66
69
|
|
67
70
|
def response_for(status)
|
@@ -73,12 +76,12 @@ module OpenapiFirst
|
|
73
76
|
end
|
74
77
|
|
75
78
|
def name
|
76
|
-
"#{method.upcase} #{path} (#{operation_id})"
|
79
|
+
@name ||= "#{method.upcase} #{path} (#{operation_id})"
|
77
80
|
end
|
78
81
|
|
79
82
|
def valid_request_content_type?(request_content_type)
|
80
83
|
content = operation_object.dig('requestBody', 'content')
|
81
|
-
return unless content
|
84
|
+
return false unless content
|
82
85
|
|
83
86
|
!!find_content_for_content_type(content, request_content_type)
|
84
87
|
end
|
@@ -91,6 +94,17 @@ module OpenapiFirst
|
|
91
94
|
@path_parameters ||= all_parameters.filter { |p| p['in'] == 'path' }
|
92
95
|
end
|
93
96
|
|
97
|
+
IGNORED_HEADERS = Set['Content-Type', 'Accept', 'Authorization'].freeze
|
98
|
+
private_constant :IGNORED_HEADERS
|
99
|
+
|
100
|
+
def header_parameters
|
101
|
+
@header_parameters ||= all_parameters.filter { |p| p['in'] == 'header' && !IGNORED_HEADERS.include?(p['name']) }
|
102
|
+
end
|
103
|
+
|
104
|
+
def cookie_parameters
|
105
|
+
@cookie_parameters ||= all_parameters.filter { |p| p['in'] == 'cookie' }
|
106
|
+
end
|
107
|
+
|
94
108
|
def all_parameters
|
95
109
|
@all_parameters ||= begin
|
96
110
|
parameters = @path_item_object['parameters']&.dup || []
|
@@ -100,6 +114,11 @@ module OpenapiFirst
|
|
100
114
|
end
|
101
115
|
end
|
102
116
|
|
117
|
+
# visibility: private
|
118
|
+
def schemas
|
119
|
+
@schemas ||= OperationSchemas.new(self)
|
120
|
+
end
|
121
|
+
|
103
122
|
private
|
104
123
|
|
105
124
|
def response_by_code(status)
|
@@ -0,0 +1,52 @@
|
|
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
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
module Plugins
|
5
|
+
ERROR_RESPONSES = {} # rubocop:disable Style/MutableConstant
|
6
|
+
|
7
|
+
def self.register_error_response(name, klass)
|
8
|
+
ERROR_RESPONSES[name] = klass
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.find_error_response(name)
|
12
|
+
return name if name.is_a?(Class)
|
13
|
+
|
14
|
+
ERROR_RESPONSES.fetch(name)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
class RequestBodyValidator
|
5
|
+
def initialize(operation, env)
|
6
|
+
@operation = operation
|
7
|
+
@env = env
|
8
|
+
@parsed_request_body = env[REQUEST_BODY]
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate!
|
12
|
+
content_type = Rack::Request.new(@env).content_type
|
13
|
+
validate_request_content_type!(@operation, content_type)
|
14
|
+
validate_request_body!(@operation, @parsed_request_body, content_type)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def validate_request_content_type!(operation, content_type)
|
20
|
+
operation.valid_request_content_type?(content_type) || OpenapiFirst.error!(415)
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate_request_body!(operation, body, content_type)
|
24
|
+
validate_request_body_presence!(body, operation)
|
25
|
+
return if content_type.nil?
|
26
|
+
|
27
|
+
schema = operation&.request_body_schema(content_type)
|
28
|
+
return unless schema
|
29
|
+
|
30
|
+
validation_result = schema.validate(body)
|
31
|
+
OpenapiFirst.error!(400, :request_body, validation_result:) if validation_result.error?
|
32
|
+
body
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate_request_body_presence!(body, operation)
|
36
|
+
return unless operation.request_body['required'] && body.nil?
|
37
|
+
|
38
|
+
OpenapiFirst.error!(400, :request_body, title: 'Request body is required')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -3,7 +3,9 @@
|
|
3
3
|
require 'rack'
|
4
4
|
require 'multi_json'
|
5
5
|
require_relative 'use_router'
|
6
|
-
require_relative '
|
6
|
+
require_relative 'error_response'
|
7
|
+
require_relative 'request_body_validator'
|
8
|
+
require_relative 'string_keyed_hash'
|
7
9
|
require 'openapi_parameters'
|
8
10
|
|
9
11
|
module OpenapiFirst
|
@@ -13,118 +15,98 @@ module OpenapiFirst
|
|
13
15
|
def initialize(app, options = {})
|
14
16
|
@app = app
|
15
17
|
@raise = options.fetch(:raise_error, false)
|
18
|
+
@error_response_class =
|
19
|
+
Plugins.find_error_response(options.fetch(:error_response, Config.default_options.error_response))
|
16
20
|
end
|
17
21
|
|
18
|
-
def call(env)
|
22
|
+
def call(env)
|
19
23
|
operation = env[OPERATION]
|
20
24
|
return @app.call(env) unless operation
|
21
25
|
|
22
|
-
error =
|
23
|
-
query_params = OpenapiParameters::Query.new(operation.query_parameters).unpack(env['QUERY_STRING'])
|
24
|
-
validate_query_parameters!(operation, query_params)
|
25
|
-
env[PARAMS].merge!(query_params)
|
26
|
-
|
27
|
-
return @app.call(env) unless operation.request_body
|
28
|
-
|
29
|
-
content_type = Rack::Request.new(env).content_type
|
30
|
-
validate_request_content_type!(operation, content_type)
|
31
|
-
parsed_request_body = env[REQUEST_BODY]
|
32
|
-
validate_request_body!(operation, parsed_request_body, content_type)
|
33
|
-
nil
|
34
|
-
end
|
26
|
+
error = validate_request(operation, env)
|
35
27
|
if error
|
36
|
-
|
28
|
+
location, title = error.values_at(:location, :title)
|
29
|
+
raise RequestInvalidError, error_message(title, location) if @raise
|
37
30
|
|
38
|
-
return
|
31
|
+
return error_response(error).render
|
39
32
|
end
|
40
33
|
@app.call(env)
|
41
34
|
end
|
42
35
|
|
43
36
|
private
|
44
37
|
|
45
|
-
def
|
46
|
-
|
47
|
-
return if content_type.nil?
|
38
|
+
def error_message(title, location)
|
39
|
+
return title unless location
|
48
40
|
|
49
|
-
|
50
|
-
return unless schema
|
51
|
-
|
52
|
-
errors = schema.validate(body)
|
53
|
-
throw_error(400, serialize_request_body_errors(errors)) if errors.any?
|
54
|
-
body
|
55
|
-
end
|
56
|
-
|
57
|
-
def validate_request_content_type!(operation, content_type)
|
58
|
-
operation.valid_request_content_type?(content_type) || throw_error(415)
|
41
|
+
"#{TOPICS.fetch(location)} #{title}"
|
59
42
|
end
|
60
43
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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)
|
65
55
|
end
|
66
56
|
|
67
|
-
def
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
57
|
+
def validate_request(operation, env)
|
58
|
+
catch(:error) do
|
59
|
+
env[PARAMS] = {}
|
60
|
+
validate_query_params!(operation, env)
|
61
|
+
validate_path_params!(operation, env)
|
62
|
+
validate_cookie_params!(operation, env)
|
63
|
+
validate_header_params!(operation, env)
|
64
|
+
RequestBodyValidator.new(operation, env).validate! if operation.request_body
|
65
|
+
nil
|
66
|
+
end
|
72
67
|
end
|
73
68
|
|
74
|
-
def
|
75
|
-
|
76
|
-
|
77
|
-
errors: errors
|
78
|
-
}
|
79
|
-
end
|
69
|
+
def validate_path_params!(operation, env)
|
70
|
+
path_parameters = operation.path_parameters
|
71
|
+
return if path_parameters.empty?
|
80
72
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
)
|
73
|
+
hashy = StringKeyedHash.new(env[Router::RAW_PATH_PARAMS])
|
74
|
+
unpacked_path_params = OpenapiParameters::Path.new(path_parameters).unpack(hashy)
|
75
|
+
validation_result = operation.schemas.path_parameters_schema.validate(unpacked_path_params)
|
76
|
+
OpenapiFirst.error!(400, :path, validation_result:) if validation_result.error?
|
77
|
+
env[PATH_PARAMS] = unpacked_path_params
|
78
|
+
env[PARAMS].merge!(unpacked_path_params)
|
87
79
|
end
|
88
80
|
|
89
|
-
def
|
90
|
-
|
91
|
-
|
92
|
-
source: {
|
93
|
-
pointer: error['data_pointer']
|
94
|
-
}
|
95
|
-
}.update(ValidationFormat.error_details(error))
|
96
|
-
end
|
97
|
-
end
|
81
|
+
def validate_query_params!(operation, env)
|
82
|
+
query_parameters = operation.query_parameters
|
83
|
+
return if operation.query_parameters.empty?
|
98
84
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
}
|
105
|
-
parameter_defs.each_with_object(init_schema) do |parameter_def, schema|
|
106
|
-
parameter = OpenapiParameters::Parameter.new(parameter_def)
|
107
|
-
schema['properties'][parameter.name] = parameter.schema if parameter.schema
|
108
|
-
schema['required'] << parameter.name if parameter.required?
|
109
|
-
end
|
85
|
+
unpacked_query_params = OpenapiParameters::Query.new(query_parameters).unpack(env['QUERY_STRING'])
|
86
|
+
validation_result = operation.schemas.query_parameters_schema.validate(unpacked_query_params)
|
87
|
+
OpenapiFirst.error!(400, :query, validation_result:) if validation_result.error?
|
88
|
+
env[QUERY_PARAMS] = unpacked_query_params
|
89
|
+
env[PARAMS].merge!(unpacked_query_params)
|
110
90
|
end
|
111
91
|
|
112
|
-
def
|
113
|
-
|
114
|
-
return unless
|
92
|
+
def validate_cookie_params!(operation, env)
|
93
|
+
cookie_parameters = operation.cookie_parameters
|
94
|
+
return unless cookie_parameters&.any?
|
115
95
|
|
116
|
-
|
117
|
-
|
118
|
-
|
96
|
+
unpacked_params = OpenapiParameters::Cookie.new(cookie_parameters).unpack(env['HTTP_COOKIE'])
|
97
|
+
validation_result = operation.schemas.cookie_parameters_schema.validate(unpacked_params)
|
98
|
+
OpenapiFirst.error!(400, :cookie, validation_result:) if validation_result.error?
|
99
|
+
env[COOKIE_PARAMS] = unpacked_params
|
119
100
|
end
|
120
101
|
|
121
|
-
def
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
102
|
+
def validate_header_params!(operation, env)
|
103
|
+
header_parameters = operation.header_parameters
|
104
|
+
return if header_parameters.empty?
|
105
|
+
|
106
|
+
unpacked_header_params = OpenapiParameters::Header.new(header_parameters).unpack_env(env)
|
107
|
+
validation_result = operation.schemas.header_parameters_schema.validate(unpacked_header_params)
|
108
|
+
OpenapiFirst.error!(400, :header, validation_result:) if validation_result.error?
|
109
|
+
env[HEADER_PARAMS] = unpacked_header_params
|
128
110
|
end
|
129
111
|
end
|
130
112
|
end
|
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
require 'multi_json'
|
4
4
|
require_relative 'use_router'
|
5
|
-
require_relative 'validation_format'
|
6
5
|
|
7
6
|
module OpenapiFirst
|
8
7
|
class ResponseValidation
|
@@ -26,8 +25,9 @@ module OpenapiFirst
|
|
26
25
|
return validate_status_only(operation, status) if status == 204
|
27
26
|
|
28
27
|
content_type = headers[Rack::CONTENT_TYPE]
|
29
|
-
response_schema = operation.
|
28
|
+
response_schema = operation.response_body_schema(status, content_type)
|
30
29
|
validate_response_body(response_schema, body) if response_schema
|
30
|
+
validate_response_headers(operation, status, headers)
|
31
31
|
end
|
32
32
|
|
33
33
|
private
|
@@ -40,14 +40,45 @@ module OpenapiFirst
|
|
40
40
|
full_body = +''
|
41
41
|
response.each { |chunk| full_body << chunk }
|
42
42
|
data = full_body.empty? ? {} : load_json(full_body)
|
43
|
-
|
44
|
-
|
45
|
-
|
43
|
+
validation = schema.validate(data)
|
44
|
+
raise ResponseBodyInvalidError, validation.message if validation.error?
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate_response_headers(operation, status, response_headers)
|
48
|
+
response_header_definitions = operation.response_for(status)&.dig('headers')
|
49
|
+
return unless response_header_definitions
|
50
|
+
|
51
|
+
unpacked_headers = unpack_response_headers(response_header_definitions, response_headers)
|
52
|
+
response_header_definitions.each do |name, definition|
|
53
|
+
next if name == 'Content-Type'
|
54
|
+
|
55
|
+
validate_response_header(name, definition, unpacked_headers, openapi_version: operation.openapi_version)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate_response_header(name, definition, unpacked_headers, openapi_version:)
|
60
|
+
unless unpacked_headers.key?(name)
|
61
|
+
raise ResponseHeaderInvalidError, "Required response header '#{name}' is missing" if definition['required']
|
62
|
+
|
63
|
+
return
|
64
|
+
end
|
65
|
+
|
66
|
+
return unless definition.key?('schema')
|
67
|
+
|
68
|
+
validation = SchemaValidation.new(definition['schema'], openapi_version:)
|
69
|
+
value = unpacked_headers[name]
|
70
|
+
validation_result = validation.validate(value)
|
71
|
+
raise ResponseHeaderInvalidError, validation_result.message if validation_result.error?
|
72
|
+
end
|
73
|
+
|
74
|
+
def unpack_response_headers(response_header_definitions, response_headers)
|
75
|
+
headers_as_parameters = response_header_definitions.map do |name, definition|
|
76
|
+
definition.merge('name' => name)
|
46
77
|
end
|
47
|
-
|
78
|
+
OpenapiParameters::Header.new(headers_as_parameters).unpack(response_headers)
|
48
79
|
end
|
49
80
|
|
50
|
-
def
|
81
|
+
def format_response_error(error)
|
51
82
|
return "Write-only field appears in response: #{error['data_pointer']}" if error['type'] == 'writeOnly'
|
52
83
|
|
53
84
|
JSONSchemer::Errors.pretty(error)
|
@@ -7,7 +7,7 @@ module OpenapiFirst
|
|
7
7
|
class ResponseValidator
|
8
8
|
def initialize(spec)
|
9
9
|
@spec = spec
|
10
|
-
@router = Router.new(->(_env) {}, spec
|
10
|
+
@router = Router.new(->(_env) {}, spec:, raise_error: true)
|
11
11
|
@response_validation = ResponseValidation.new(->(response) { response.to_a })
|
12
12
|
end
|
13
13
|
|