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
@@ -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
|