openapi_first 1.0.0.beta6 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -1,113 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'multi_json'
|
4
|
-
require_relative 'use_router'
|
5
|
-
|
6
|
-
module OpenapiFirst
|
7
|
-
class ResponseValidation
|
8
|
-
prepend UseRouter
|
9
|
-
|
10
|
-
def initialize(app, _options = {})
|
11
|
-
@app = app
|
12
|
-
end
|
13
|
-
|
14
|
-
def call(env)
|
15
|
-
operation = env[OPERATION]
|
16
|
-
return @app.call(env) unless operation
|
17
|
-
|
18
|
-
response = @app.call(env)
|
19
|
-
validate(response, operation)
|
20
|
-
response
|
21
|
-
end
|
22
|
-
|
23
|
-
def validate(response, operation)
|
24
|
-
status, headers, body = response.to_a
|
25
|
-
response_definition = response_for(operation, status)
|
26
|
-
|
27
|
-
validate_response_headers(response_definition.headers, headers, openapi_version: operation.openapi_version)
|
28
|
-
|
29
|
-
return if no_content?(response_definition)
|
30
|
-
|
31
|
-
content_type = Rack::Response[status, headers, body].content_type
|
32
|
-
raise ResponseInvalid, "Response has no content-type for '#{operation.name}'" unless content_type
|
33
|
-
|
34
|
-
response_schema = response_definition.schema_for(content_type)
|
35
|
-
unless response_schema
|
36
|
-
message = "Response content type not found '#{content_type}' for '#{operation.name}'"
|
37
|
-
raise ResponseContentTypeNotFoundError, message
|
38
|
-
end
|
39
|
-
validate_response_body(response_schema, body)
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
def no_content?(response_definition)
|
45
|
-
response_definition.status == 204 || !response_definition.content?
|
46
|
-
end
|
47
|
-
|
48
|
-
def response_for(operation, status)
|
49
|
-
response = operation.response_for(status)
|
50
|
-
return response if response
|
51
|
-
|
52
|
-
message = "Response status code or default not found: #{status} for '#{operation.name}'"
|
53
|
-
raise OpenapiFirst::ResponseCodeNotFoundError, message
|
54
|
-
end
|
55
|
-
|
56
|
-
def validate_status_only(operation, status)
|
57
|
-
response_for(operation, status)
|
58
|
-
end
|
59
|
-
|
60
|
-
def validate_response_body(schema, response)
|
61
|
-
full_body = +''
|
62
|
-
response.each { |chunk| full_body << chunk }
|
63
|
-
data = full_body.empty? ? {} : load_json(full_body)
|
64
|
-
validation = schema.validate(data)
|
65
|
-
raise ResponseBodyInvalidError, validation.message if validation.error?
|
66
|
-
end
|
67
|
-
|
68
|
-
def validate_response_headers(response_header_definitions, response_headers, openapi_version:)
|
69
|
-
return unless response_header_definitions
|
70
|
-
|
71
|
-
unpacked_headers = unpack_response_headers(response_header_definitions, response_headers)
|
72
|
-
response_header_definitions.each do |name, definition|
|
73
|
-
next if name == 'Content-Type'
|
74
|
-
|
75
|
-
validate_response_header(name, definition, unpacked_headers, openapi_version:)
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def validate_response_header(name, definition, unpacked_headers, openapi_version:)
|
80
|
-
unless unpacked_headers.key?(name)
|
81
|
-
raise ResponseHeaderInvalidError, "Required response header '#{name}' is missing" if definition['required']
|
82
|
-
|
83
|
-
return
|
84
|
-
end
|
85
|
-
|
86
|
-
return unless definition.key?('schema')
|
87
|
-
|
88
|
-
validation = Schema.new(definition['schema'], openapi_version:)
|
89
|
-
value = unpacked_headers[name]
|
90
|
-
schema_validation = validation.validate(value)
|
91
|
-
raise ResponseHeaderInvalidError, schema_validation.message if schema_validation.error?
|
92
|
-
end
|
93
|
-
|
94
|
-
def unpack_response_headers(response_header_definitions, response_headers)
|
95
|
-
headers_as_parameters = response_header_definitions.map do |name, definition|
|
96
|
-
definition.merge('name' => name, 'in' => 'header')
|
97
|
-
end
|
98
|
-
OpenapiParameters::Header.new(headers_as_parameters).unpack(response_headers)
|
99
|
-
end
|
100
|
-
|
101
|
-
def format_response_error(error)
|
102
|
-
return "Write-only field appears in response: #{error['data_pointer']}" if error['type'] == 'writeOnly'
|
103
|
-
|
104
|
-
JSONSchemer::Errors.pretty(error)
|
105
|
-
end
|
106
|
-
|
107
|
-
def load_json(string)
|
108
|
-
MultiJson.load(string)
|
109
|
-
rescue MultiJson::ParseError
|
110
|
-
string
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
@@ -1,21 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'response_validation'
|
4
|
-
require_relative 'router'
|
5
|
-
|
6
|
-
module OpenapiFirst
|
7
|
-
# A class to run manual response validation
|
8
|
-
class ResponseValidator
|
9
|
-
def initialize(spec)
|
10
|
-
@spec = spec
|
11
|
-
@router = Router.new(->(_env) {}, spec:, raise_error: true)
|
12
|
-
@response_validation = ResponseValidation.new(->(response) { response.to_a })
|
13
|
-
end
|
14
|
-
|
15
|
-
def validate(request, response)
|
16
|
-
env = request.env.dup
|
17
|
-
@router.call(env)
|
18
|
-
@response_validation.validate(response, env[OPERATION])
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
data/lib/openapi_first/router.rb
DELETED
@@ -1,68 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rack'
|
4
|
-
require 'multi_json'
|
5
|
-
require 'mustermann'
|
6
|
-
require_relative 'body_parser'
|
7
|
-
|
8
|
-
module OpenapiFirst
|
9
|
-
class Router
|
10
|
-
# The unconverted path parameters before they are converted to the types defined in the API description
|
11
|
-
RAW_PATH_PARAMS = 'openapi.raw_path_params'
|
12
|
-
|
13
|
-
NOT_FOUND = Rack::Response.new('Not Found', 404).finish.freeze
|
14
|
-
METHOD_NOT_ALLOWED = Rack::Response.new('Method Not Allowed', 405).finish.freeze
|
15
|
-
|
16
|
-
def initialize(
|
17
|
-
app,
|
18
|
-
options
|
19
|
-
)
|
20
|
-
@app = app
|
21
|
-
@raise = options.fetch(:raise_error, false)
|
22
|
-
@not_found = options.fetch(:not_found, :halt)
|
23
|
-
@error_response_class = options.fetch(:error_response, Config.default_options.error_response)
|
24
|
-
spec = options.fetch(:spec)
|
25
|
-
raise "You have to pass spec: when initializing #{self.class}" unless spec
|
26
|
-
|
27
|
-
@definition = spec.is_a?(Definition) ? spec : OpenapiFirst.load(spec)
|
28
|
-
@filepath = @definition.filepath
|
29
|
-
end
|
30
|
-
|
31
|
-
def call(env)
|
32
|
-
env[OPERATION] = nil
|
33
|
-
request = Rack::Request.new(env)
|
34
|
-
path_item, path_params = @definition.find_path_item_and_params(request.path)
|
35
|
-
operation = path_item&.find_operation(request.request_method.downcase)
|
36
|
-
|
37
|
-
env[OPERATION] = operation
|
38
|
-
env[RAW_PATH_PARAMS] = path_params
|
39
|
-
|
40
|
-
if operation.nil?
|
41
|
-
raise_error(env) if @raise
|
42
|
-
return @app.call(env) if @not_found == :continue
|
43
|
-
end
|
44
|
-
|
45
|
-
return NOT_FOUND unless path_item
|
46
|
-
return METHOD_NOT_ALLOWED unless operation
|
47
|
-
|
48
|
-
@app.call(env)
|
49
|
-
end
|
50
|
-
|
51
|
-
ORIGINAL_PATH = 'openapi_first.path_info'
|
52
|
-
private_constant :ORIGINAL_PATH
|
53
|
-
|
54
|
-
ROUTER_PARSED_BODY = 'router.parsed_body'
|
55
|
-
private_constant :ROUTER_PARSED_BODY
|
56
|
-
|
57
|
-
private
|
58
|
-
|
59
|
-
def raise_error(env)
|
60
|
-
req = Rack::Request.new(env)
|
61
|
-
msg =
|
62
|
-
"Could not find definition for #{req.request_method} '#{
|
63
|
-
req.path
|
64
|
-
}' in API description #{@filepath}"
|
65
|
-
raise NotFoundError, msg
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
@@ -1,18 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module OpenapiFirst
|
4
|
-
module UseRouter
|
5
|
-
def initialize(app, options = {})
|
6
|
-
@app = app
|
7
|
-
@options = options
|
8
|
-
super
|
9
|
-
end
|
10
|
-
|
11
|
-
def call(env)
|
12
|
-
return super if env.key?(OPERATION)
|
13
|
-
|
14
|
-
@router ||= Router.new(->(e) { super(e) }, @options)
|
15
|
-
@router.call(env)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|