openapi_first 0.20.0 → 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +20 -2
- data/Gemfile.lock +43 -34
- data/README.md +26 -177
- data/benchmarks/Gemfile.lock +54 -54
- data/benchmarks/apps/openapi_first.ru +1 -1
- data/benchmarks/benchmarks.rb +2 -1
- data/examples/app.rb +12 -16
- data/lib/openapi_first/body_parser_middleware.rb +53 -0
- data/lib/openapi_first/errors.rb +2 -0
- data/lib/openapi_first/operation.rb +25 -53
- data/lib/openapi_first/request_validation.rb +53 -96
- data/lib/openapi_first/router.rb +47 -17
- data/lib/openapi_first/schema_validation.rb +9 -0
- data/lib/openapi_first/use_router.rb +1 -3
- data/lib/openapi_first/utils.rb +11 -5
- data/lib/openapi_first/version.rb +1 -1
- data/lib/openapi_first.rb +3 -35
- data/openapi_first.gemspec +6 -4
- metadata +56 -23
- data/lib/openapi_first/app.rb +0 -29
- data/lib/openapi_first/coverage.rb +0 -28
- data/lib/openapi_first/default_operation_resolver.rb +0 -63
- data/lib/openapi_first/inbox.rb +0 -13
- data/lib/openapi_first/rack_responder.rb +0 -12
- data/lib/openapi_first/responder.rb +0 -44
- data/lib/openapi_first/response_object.rb +0 -20
- data/lib/openapi_first/validation.rb +0 -15
data/benchmarks/benchmarks.rb
CHANGED
@@ -18,7 +18,8 @@ examples = [
|
|
18
18
|
[Rack::MockRequest.env_for('/hello?filter[id]=1,2'), 200]
|
19
19
|
]
|
20
20
|
|
21
|
-
|
21
|
+
glob = ARGV[0] || './apps/*.ru'
|
22
|
+
apps = Dir[glob].each_with_object({}) do |config, hash|
|
22
23
|
hash[config] = Rack::Builder.parse_file(config).first
|
23
24
|
end
|
24
25
|
apps.freeze
|
data/examples/app.rb
CHANGED
@@ -1,22 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'openapi_first'
|
4
|
+
require 'rack'
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
6
|
+
# This example is a bit contrived, but it shows what you could do with the middlewares
|
7
|
+
|
8
|
+
App = Rack::Builder.new do
|
9
|
+
use OpenapiFirst::RequestValidation, raise_error: true, spec: File.expand_path('./openapi.yaml', __dir__)
|
10
|
+
use OpenapiFirst::ResponseValidation
|
14
11
|
|
15
|
-
|
12
|
+
handlers = {
|
13
|
+
'things#index' => ->(_env) { [200, { 'Content-Type' => 'application/json' }, ['{"hello": "world"}']] }
|
14
|
+
}
|
15
|
+
not_found = ->(_env) { [404, {}, []] }
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
namespace: Web,
|
20
|
-
router_raise_error: OpenapiFirst.env == 'test',
|
21
|
-
response_validation: OpenapiFirst.env == 'test'
|
22
|
-
)
|
17
|
+
run ->(env) { handlers.fetch(env[OpenapiFirst::OPERATION].operation_id, not_found).call(env) }
|
18
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'multi_json'
|
4
|
+
|
5
|
+
module OpenapiFirst
|
6
|
+
class BodyParserMiddleware
|
7
|
+
def initialize(app, options = {})
|
8
|
+
@app = app
|
9
|
+
@raise = options.fetch(:raise_error, false)
|
10
|
+
end
|
11
|
+
|
12
|
+
RACK_INPUT = 'rack.input'
|
13
|
+
ROUTER_PARSED_BODY = 'router.parsed_body'
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
env[ROUTER_PARSED_BODY] = parse_body(env)
|
17
|
+
@app.call(env)
|
18
|
+
rescue BodyParsingError => e
|
19
|
+
raise if @raise
|
20
|
+
|
21
|
+
err = { title: "Failed to parse body as #{env['CONTENT_TYPE']}", status: '400' }
|
22
|
+
err[:detail] = e.cause unless ENV['RACK_ENV'] == 'production'
|
23
|
+
errors = [err]
|
24
|
+
|
25
|
+
Rack::Response.new(
|
26
|
+
MultiJson.dump(errors: errors),
|
27
|
+
400,
|
28
|
+
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
29
|
+
).finish
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def parse_body(env)
|
35
|
+
request = Rack::Request.new(env)
|
36
|
+
body = read_body(request)
|
37
|
+
return if body.empty?
|
38
|
+
|
39
|
+
return MultiJson.load(body) if request.media_type =~ (/json/i) && (request.media_type =~ /json/i)
|
40
|
+
return request.POST if request.form_data?
|
41
|
+
|
42
|
+
body
|
43
|
+
rescue MultiJson::ParseError => e
|
44
|
+
raise BodyParsingError, e
|
45
|
+
end
|
46
|
+
|
47
|
+
def read_body(request)
|
48
|
+
body = request.body.read
|
49
|
+
request.body.rewind
|
50
|
+
body
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/openapi_first/errors.rb
CHANGED
@@ -4,10 +4,9 @@ require 'forwardable'
|
|
4
4
|
require 'set'
|
5
5
|
require_relative 'schema_validation'
|
6
6
|
require_relative 'utils'
|
7
|
-
require_relative 'response_object'
|
8
7
|
|
9
8
|
module OpenapiFirst
|
10
|
-
class Operation
|
9
|
+
class Operation
|
11
10
|
extend Forwardable
|
12
11
|
def_delegators :operation_object,
|
13
12
|
:[],
|
@@ -40,17 +39,6 @@ module OpenapiFirst
|
|
40
39
|
operation_object['requestBody']
|
41
40
|
end
|
42
41
|
|
43
|
-
def parameters_schema
|
44
|
-
@parameters_schema ||= begin
|
45
|
-
parameters_json_schema = build_parameters_json_schema
|
46
|
-
parameters_json_schema && SchemaValidation.new(parameters_json_schema)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def content_types_for(status)
|
51
|
-
response_for(status)['content']&.keys
|
52
|
-
end
|
53
|
-
|
54
42
|
def response_schema_for(status, content_type)
|
55
43
|
content = response_for(status)['content']
|
56
44
|
return if content.nil? || content.empty?
|
@@ -88,6 +76,30 @@ module OpenapiFirst
|
|
88
76
|
"#{method.upcase} #{path} (#{operation_id})"
|
89
77
|
end
|
90
78
|
|
79
|
+
def valid_request_content_type?(request_content_type)
|
80
|
+
content = operation_object.dig('requestBody', 'content')
|
81
|
+
return unless content
|
82
|
+
|
83
|
+
!!find_content_for_content_type(content, request_content_type)
|
84
|
+
end
|
85
|
+
|
86
|
+
def query_parameters
|
87
|
+
@query_parameters ||= all_parameters.filter { |p| p['in'] == 'query' }
|
88
|
+
end
|
89
|
+
|
90
|
+
def path_parameters
|
91
|
+
@path_parameters ||= all_parameters.filter { |p| p['in'] == 'path' }
|
92
|
+
end
|
93
|
+
|
94
|
+
def all_parameters
|
95
|
+
@all_parameters ||= begin
|
96
|
+
parameters = @path_item_object['parameters']&.dup || []
|
97
|
+
parameters_on_operation = operation_object['parameters']
|
98
|
+
parameters.concat(parameters_on_operation) if parameters_on_operation
|
99
|
+
parameters
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
91
103
|
private
|
92
104
|
|
93
105
|
def response_by_code(status)
|
@@ -107,45 +119,5 @@ module OpenapiFirst
|
|
107
119
|
content[type] || content["#{type.split('/')[0]}/*"] || content['*/*']
|
108
120
|
end
|
109
121
|
end
|
110
|
-
|
111
|
-
def build_parameters_json_schema
|
112
|
-
parameters = all_parameters
|
113
|
-
return unless parameters&.any?
|
114
|
-
|
115
|
-
parameters.each_with_object(new_node) do |parameter, schema|
|
116
|
-
params = Rack::Utils.parse_nested_query(parameter['name'])
|
117
|
-
generate_schema(schema, params, parameter)
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
def all_parameters
|
122
|
-
parameters = @path_item_object['parameters']&.dup || []
|
123
|
-
parameters_on_operation = operation_object['parameters']
|
124
|
-
parameters.concat(parameters_on_operation) if parameters_on_operation
|
125
|
-
parameters
|
126
|
-
end
|
127
|
-
|
128
|
-
def generate_schema(schema, params, parameter)
|
129
|
-
required = Set.new(schema['required'])
|
130
|
-
params.each do |key, value|
|
131
|
-
required << key if parameter['required']
|
132
|
-
if value.is_a? Hash
|
133
|
-
property_schema = new_node
|
134
|
-
generate_schema(property_schema, value, parameter)
|
135
|
-
Utils.deep_merge!(schema['properties'], { key => property_schema })
|
136
|
-
else
|
137
|
-
schema['properties'][key] = parameter['schema']
|
138
|
-
end
|
139
|
-
end
|
140
|
-
schema['required'] = required.to_a
|
141
|
-
end
|
142
|
-
|
143
|
-
def new_node
|
144
|
-
{
|
145
|
-
'type' => 'object',
|
146
|
-
'required' => [],
|
147
|
-
'properties' => {}
|
148
|
-
}
|
149
|
-
end
|
150
122
|
end
|
151
123
|
end
|
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
require 'rack'
|
4
4
|
require 'multi_json'
|
5
|
-
require_relative 'inbox'
|
6
5
|
require_relative 'use_router'
|
7
6
|
require_relative 'validation_format'
|
7
|
+
require 'openapi_parameters'
|
8
8
|
|
9
9
|
module OpenapiFirst
|
10
|
-
class RequestValidation
|
10
|
+
class RequestValidation
|
11
11
|
prepend UseRouter
|
12
12
|
|
13
13
|
def initialize(app, options = {})
|
@@ -19,58 +19,49 @@ module OpenapiFirst
|
|
19
19
|
operation = env[OPERATION]
|
20
20
|
return @app.call(env) unless operation
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
validate_query_parameters!(
|
25
|
-
|
26
|
-
|
22
|
+
error = catch(:error) do
|
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
27
|
return @app.call(env) unless operation.request_body
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
35
|
+
if error
|
36
|
+
raise RequestInvalidError, error[:errors] if @raise
|
37
|
+
|
38
|
+
return validation_error_response(error[:status], error[:errors])
|
34
39
|
end
|
40
|
+
@app.call(env)
|
35
41
|
end
|
36
42
|
|
37
43
|
private
|
38
44
|
|
39
|
-
def
|
40
|
-
throw :halt, response
|
41
|
-
end
|
42
|
-
|
43
|
-
def parse_and_validate_request_body!(env, content_type, body, operation)
|
45
|
+
def validate_request_body!(operation, body, content_type)
|
44
46
|
validate_request_body_presence!(body, operation)
|
45
|
-
return if
|
47
|
+
return if content_type.nil?
|
46
48
|
|
47
49
|
schema = operation&.request_body_schema(content_type)
|
48
50
|
return unless schema
|
49
51
|
|
50
|
-
|
51
|
-
errors
|
52
|
-
|
53
|
-
env[INBOX].merge! env[REQUEST_BODY] = Utils.deep_symbolize(parsed_request_body)
|
54
|
-
end
|
55
|
-
|
56
|
-
def parse_request_body!(body)
|
57
|
-
MultiJson.load(body)
|
58
|
-
rescue MultiJson::ParseError => e
|
59
|
-
err = { title: 'Failed to parse body as JSON' }
|
60
|
-
err[:detail] = e.cause unless ENV['RACK_ENV'] == 'production'
|
61
|
-
halt_with_error(400, [err])
|
52
|
+
errors = schema.validate(body)
|
53
|
+
throw_error(400, serialize_request_body_errors(errors)) if errors.any?
|
54
|
+
body
|
62
55
|
end
|
63
56
|
|
64
|
-
def validate_request_content_type!(
|
65
|
-
|
66
|
-
|
67
|
-
halt_with_error(415)
|
57
|
+
def validate_request_content_type!(operation, content_type)
|
58
|
+
operation.valid_request_content_type?(content_type) || throw_error(415)
|
68
59
|
end
|
69
60
|
|
70
61
|
def validate_request_body_presence!(body, operation)
|
71
|
-
return unless operation.request_body['required'] && body.
|
62
|
+
return unless operation.request_body['required'] && body.nil?
|
72
63
|
|
73
|
-
|
64
|
+
throw_error(415, 'Request body is required')
|
74
65
|
end
|
75
66
|
|
76
67
|
def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
|
@@ -80,10 +71,15 @@ module OpenapiFirst
|
|
80
71
|
}
|
81
72
|
end
|
82
73
|
|
83
|
-
def
|
84
|
-
|
74
|
+
def throw_error(status, errors = [default_error(status)])
|
75
|
+
throw :error, {
|
76
|
+
status: status,
|
77
|
+
errors: errors
|
78
|
+
}
|
79
|
+
end
|
85
80
|
|
86
|
-
|
81
|
+
def validation_error_response(status, errors)
|
82
|
+
Rack::Response.new(
|
87
83
|
MultiJson.dump(errors: errors),
|
88
84
|
status,
|
89
85
|
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
@@ -100,32 +96,29 @@ module OpenapiFirst
|
|
100
96
|
end
|
101
97
|
end
|
102
98
|
|
103
|
-
def
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
99
|
+
def build_json_schema(parameter_defs)
|
100
|
+
init_schema = {
|
101
|
+
'type' => 'object',
|
102
|
+
'properties' => {},
|
103
|
+
'required' => []
|
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
|
114
110
|
end
|
115
111
|
|
116
|
-
def
|
117
|
-
|
118
|
-
|
119
|
-
parameter_name = key_value[0].to_sym
|
120
|
-
schema = key_value[1]
|
121
|
-
next unless params.key?(parameter_name)
|
112
|
+
def validate_query_parameters!(operation, params)
|
113
|
+
parameter_defs = operation.query_parameters
|
114
|
+
return unless parameter_defs&.any?
|
122
115
|
|
123
|
-
|
124
|
-
|
125
|
-
|
116
|
+
json_schema = build_json_schema(parameter_defs)
|
117
|
+
errors = SchemaValidation.new(json_schema).validate(params)
|
118
|
+
throw_error(400, serialize_parameter_errors(errors)) if errors.any?
|
126
119
|
end
|
127
120
|
|
128
|
-
def
|
121
|
+
def serialize_parameter_errors(validation_errors)
|
129
122
|
validation_errors.map do |error|
|
130
123
|
pointer = error['data_pointer'][1..].to_s
|
131
124
|
{
|
@@ -133,41 +126,5 @@ module OpenapiFirst
|
|
133
126
|
}.update(ValidationFormat.error_details(error))
|
134
127
|
end
|
135
128
|
end
|
136
|
-
|
137
|
-
def parse_parameter(value, schema)
|
138
|
-
return filtered_params(schema, value) if schema['properties']
|
139
|
-
|
140
|
-
return parse_array_parameter(value, schema) if schema['type'] == 'array'
|
141
|
-
|
142
|
-
parse_simple_value(value, schema)
|
143
|
-
end
|
144
|
-
|
145
|
-
def parse_array_parameter(value, schema)
|
146
|
-
return value if value.nil? || value.empty?
|
147
|
-
|
148
|
-
array = value.is_a?(Array) ? value : value.split(',')
|
149
|
-
return array unless schema['items']
|
150
|
-
|
151
|
-
array.map! { |e| parse_simple_value(e, schema['items']) }
|
152
|
-
end
|
153
|
-
|
154
|
-
def parse_simple_value(value, schema)
|
155
|
-
return to_boolean(value) if schema['type'] == 'boolean'
|
156
|
-
|
157
|
-
begin
|
158
|
-
return Integer(value, 10) if schema['type'] == 'integer'
|
159
|
-
return Float(value) if schema['type'] == 'number'
|
160
|
-
rescue ArgumentError
|
161
|
-
value
|
162
|
-
end
|
163
|
-
value
|
164
|
-
end
|
165
|
-
|
166
|
-
def to_boolean(value)
|
167
|
-
return true if value == 'true'
|
168
|
-
return false if value == 'false'
|
169
|
-
|
170
|
-
value
|
171
|
-
end
|
172
129
|
end
|
173
130
|
end
|
data/lib/openapi_first/router.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rack'
|
4
|
+
require 'multi_json'
|
4
5
|
require 'hanami/router'
|
6
|
+
require_relative 'body_parser_middleware'
|
5
7
|
|
6
8
|
module OpenapiFirst
|
7
9
|
class Router
|
@@ -10,7 +12,6 @@ module OpenapiFirst
|
|
10
12
|
options
|
11
13
|
)
|
12
14
|
@app = app
|
13
|
-
@parent_app = options.fetch(:parent_app, nil)
|
14
15
|
@raise = options.fetch(:raise_error, false)
|
15
16
|
@not_found = options.fetch(:not_found, :halt)
|
16
17
|
spec = options.fetch(:spec)
|
@@ -26,8 +27,6 @@ module OpenapiFirst
|
|
26
27
|
env[OPERATION] = nil
|
27
28
|
response = call_router(env)
|
28
29
|
if env[OPERATION].nil?
|
29
|
-
return @parent_app.call(env) if @parent_app # This should only happen if used via OpenapiFirst.middleware
|
30
|
-
|
31
30
|
raise_error(env) if @raise
|
32
31
|
|
33
32
|
return @app.call(env) if @not_found == :continue
|
@@ -37,6 +36,10 @@ module OpenapiFirst
|
|
37
36
|
end
|
38
37
|
|
39
38
|
ORIGINAL_PATH = 'openapi_first.path_info'
|
39
|
+
private_constant :ORIGINAL_PATH
|
40
|
+
|
41
|
+
ROUTER_PARSED_BODY = 'router.parsed_body'
|
42
|
+
private_constant :ROUTER_PARSED_BODY
|
40
43
|
|
41
44
|
private
|
42
45
|
|
@@ -54,26 +57,53 @@ module OpenapiFirst
|
|
54
57
|
env[ORIGINAL_PATH] = env[Rack::PATH_INFO]
|
55
58
|
env[Rack::PATH_INFO] = Rack::Request.new(env).path
|
56
59
|
@router.call(env)
|
60
|
+
rescue BodyParsingError => e
|
61
|
+
handle_body_parsing_error(e)
|
57
62
|
ensure
|
58
63
|
env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
|
59
64
|
end
|
60
65
|
|
66
|
+
def handle_body_parsing_error(exception)
|
67
|
+
err = { title: 'Failed to parse body as application/json', status: '400' }
|
68
|
+
err[:detail] = exception.cause unless ENV['RACK_ENV'] == 'production'
|
69
|
+
errors = [err]
|
70
|
+
raise RequestInvalidError, errors if @raise
|
71
|
+
|
72
|
+
Rack::Response.new(
|
73
|
+
MultiJson.dump(errors: errors),
|
74
|
+
400,
|
75
|
+
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
76
|
+
).finish
|
77
|
+
end
|
78
|
+
|
61
79
|
def build_router(operations)
|
62
|
-
router = Hanami::Router.new
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
80
|
+
router = Hanami::Router.new.tap do |r|
|
81
|
+
operations.each do |operation|
|
82
|
+
normalized_path = operation.path.gsub('{', ':').gsub('}', '')
|
83
|
+
r.public_send(
|
84
|
+
operation.method,
|
85
|
+
normalized_path,
|
86
|
+
to: build_route(operation)
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
raise_error = @raise
|
91
|
+
Rack::Builder.app do
|
92
|
+
use BodyParserMiddleware, raise_error: raise_error
|
93
|
+
run router
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def build_route(operation)
|
98
|
+
lambda do |env|
|
99
|
+
env[OPERATION] = operation
|
100
|
+
path_info = env.delete(ORIGINAL_PATH)
|
101
|
+
env[REQUEST_BODY] = env.delete(ROUTER_PARSED_BODY) if env.key?(ROUTER_PARSED_BODY)
|
102
|
+
route_params = Utils::StringKeyedHash.new(env['router.params'])
|
103
|
+
env[PARAMS] = OpenapiParameters::Path.new(operation.path_parameters).unpack(route_params)
|
104
|
+
env[Rack::PATH_INFO] = path_info
|
105
|
+
@app.call(env)
|
75
106
|
end
|
76
|
-
router
|
77
107
|
end
|
78
108
|
end
|
79
109
|
end
|
@@ -17,6 +17,7 @@ module OpenapiFirst
|
|
17
17
|
insert_property_defaults: true,
|
18
18
|
before_property_validation: proc do |data, property, property_schema, parent|
|
19
19
|
convert_nullable(data, property, property_schema, parent)
|
20
|
+
binary_format(data, property, property_schema, parent)
|
20
21
|
end
|
21
22
|
)
|
22
23
|
end
|
@@ -27,6 +28,14 @@ module OpenapiFirst
|
|
27
28
|
|
28
29
|
private
|
29
30
|
|
31
|
+
def binary_format(data, property, property_schema, _parent)
|
32
|
+
return unless property_schema.is_a?(Hash) && property_schema['format'] == 'binary'
|
33
|
+
|
34
|
+
property_schema['type'] = 'object'
|
35
|
+
property_schema.delete('format')
|
36
|
+
data[property].transform_keys!(&:to_s)
|
37
|
+
end
|
38
|
+
|
30
39
|
def convert_nullable(_data, _property, property_schema, _parent)
|
31
40
|
return unless property_schema.is_a?(Hash) && property_schema['nullable'] && property_schema['type']
|
32
41
|
|
@@ -11,9 +11,7 @@ module OpenapiFirst
|
|
11
11
|
def call(env)
|
12
12
|
return super if env.key?(OPERATION)
|
13
13
|
|
14
|
-
@router ||= Router.new(
|
15
|
-
super(e)
|
16
|
-
}, spec: @options.fetch(:spec), raise_error: @options.fetch(:raise_error, false))
|
14
|
+
@router ||= Router.new(->(e) { super(e) }, @options)
|
17
15
|
@router.call(env)
|
18
16
|
end
|
19
17
|
end
|
data/lib/openapi_first/utils.rb
CHANGED
@@ -18,12 +18,18 @@ module OpenapiFirst
|
|
18
18
|
Hanami::Utils::String.classify(string)
|
19
19
|
end
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
class StringKeyedHash
|
22
|
+
def initialize(original)
|
23
|
+
@orig = original
|
24
|
+
end
|
25
|
+
|
26
|
+
def key?(key)
|
27
|
+
@orig.key?(key.to_sym)
|
28
|
+
end
|
24
29
|
|
25
|
-
|
26
|
-
|
30
|
+
def [](key)
|
31
|
+
@orig[key.to_sym]
|
32
|
+
end
|
27
33
|
end
|
28
34
|
end
|
29
35
|
end
|
data/lib/openapi_first.rb
CHANGED
@@ -5,19 +5,15 @@ require 'json_refs'
|
|
5
5
|
require_relative 'openapi_first/definition'
|
6
6
|
require_relative 'openapi_first/version'
|
7
7
|
require_relative 'openapi_first/errors'
|
8
|
-
require_relative 'openapi_first/inbox'
|
9
8
|
require_relative 'openapi_first/router'
|
10
9
|
require_relative 'openapi_first/request_validation'
|
11
10
|
require_relative 'openapi_first/response_validator'
|
12
11
|
require_relative 'openapi_first/response_validation'
|
13
|
-
require_relative 'openapi_first/responder'
|
14
|
-
require_relative 'openapi_first/app'
|
15
12
|
|
16
13
|
module OpenapiFirst
|
17
|
-
OPERATION = '
|
18
|
-
|
19
|
-
REQUEST_BODY = '
|
20
|
-
INBOX = 'openapi_first.inbox'
|
14
|
+
OPERATION = 'openapi.operation'
|
15
|
+
PARAMS = 'openapi.params'
|
16
|
+
REQUEST_BODY = 'openapi.parsed_request_body'
|
21
17
|
HANDLER = 'openapi_first.handler'
|
22
18
|
|
23
19
|
def self.env
|
@@ -50,32 +46,4 @@ module OpenapiFirst
|
|
50
46
|
response_validation: response_validation
|
51
47
|
)
|
52
48
|
end
|
53
|
-
|
54
|
-
def self.middleware(
|
55
|
-
spec,
|
56
|
-
namespace:,
|
57
|
-
router_raise_error: false,
|
58
|
-
request_validation_raise_error: false,
|
59
|
-
response_validation: false
|
60
|
-
)
|
61
|
-
spec = OpenapiFirst.load(spec) unless spec.is_a?(Definition)
|
62
|
-
AppWithOptions.new(
|
63
|
-
spec,
|
64
|
-
namespace: namespace,
|
65
|
-
router_raise_error: router_raise_error,
|
66
|
-
request_validation_raise_error: request_validation_raise_error,
|
67
|
-
response_validation: response_validation
|
68
|
-
)
|
69
|
-
end
|
70
|
-
|
71
|
-
class AppWithOptions
|
72
|
-
def initialize(spec, options)
|
73
|
-
@spec = spec
|
74
|
-
@options = options
|
75
|
-
end
|
76
|
-
|
77
|
-
def new(app)
|
78
|
-
App.new(app, @spec, **@options)
|
79
|
-
end
|
80
|
-
end
|
81
49
|
end
|
data/openapi_first.gemspec
CHANGED
@@ -32,17 +32,19 @@ Gem::Specification.new do |spec|
|
|
32
32
|
spec.bindir = 'exe'
|
33
33
|
spec.require_paths = ['lib']
|
34
34
|
|
35
|
-
spec.required_ruby_version = '>=
|
35
|
+
spec.required_ruby_version = '>= 3.0.5'
|
36
36
|
|
37
37
|
spec.add_runtime_dependency 'deep_merge', '>= 1.2.1'
|
38
|
-
spec.add_runtime_dependency 'hanami-router', '2.0.
|
39
|
-
spec.add_runtime_dependency 'hanami-utils', '2.0.
|
38
|
+
spec.add_runtime_dependency 'hanami-router', '~> 2.0.0'
|
39
|
+
spec.add_runtime_dependency 'hanami-utils', '~> 2.0.0'
|
40
40
|
spec.add_runtime_dependency 'json_refs', '~> 0.1', '>= 0.1.7'
|
41
41
|
spec.add_runtime_dependency 'json_schemer', '~> 0.2.16'
|
42
42
|
spec.add_runtime_dependency 'multi_json', '~> 1.14'
|
43
|
-
spec.add_runtime_dependency '
|
43
|
+
spec.add_runtime_dependency 'mustermann-contrib', '~> 3.0.0'
|
44
|
+
spec.add_runtime_dependency 'rack', '>= 2.2', '< 4.0'
|
44
45
|
|
45
46
|
spec.add_development_dependency 'bundler', '~> 2'
|
47
|
+
spec.add_development_dependency 'openapi_parameters', '~> 0.2', '<= 2.0.0'
|
46
48
|
spec.add_development_dependency 'rack-test', '~> 1'
|
47
49
|
spec.add_development_dependency 'rake', '~> 13'
|
48
50
|
spec.add_development_dependency 'rspec', '~> 3'
|