openapi_first 0.20.0 → 1.0.0.beta1
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/.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'
|