openapi_first 0.13.2 → 0.14.2
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 +6 -46
- data/CHANGELOG.md +15 -1
- data/Gemfile.lock +60 -59
- data/README.md +19 -9
- data/benchmarks/Gemfile +2 -1
- data/benchmarks/Gemfile.lock +59 -59
- data/benchmarks/apps/committee.ru +14 -18
- data/benchmarks/apps/hanami_api.ru +21 -0
- data/benchmarks/apps/hanami_router.ru +1 -1
- data/benchmarks/apps/sinatra.ru +1 -1
- data/benchmarks/apps/syro.ru +3 -3
- data/benchmarks/benchmarks.rb +11 -14
- data/lib/openapi_first/app.rb +3 -5
- data/lib/openapi_first/{find_handler.rb → default_operation_resolver.rb} +5 -11
- data/lib/openapi_first/definition.rb +9 -8
- data/lib/openapi_first/operation.rb +65 -25
- data/lib/openapi_first/request_validation.rb +14 -27
- data/lib/openapi_first/responder.rb +4 -4
- data/lib/openapi_first/response_validation.rb +2 -1
- data/lib/openapi_first/router.rb +6 -4
- data/lib/openapi_first/schema_validation.rb +36 -0
- data/lib/openapi_first/utils.rb +7 -14
- data/lib/openapi_first/validation_format.rb +22 -0
- data/lib/openapi_first/version.rb +1 -1
- data/lib/openapi_first.rb +9 -4
- data/openapi_first.gemspec +4 -2
- metadata +13 -12
- data/.travis.yml +0 -8
data/benchmarks/apps/syro.ru
CHANGED
@@ -7,17 +7,17 @@ app = Syro.new do
|
|
7
7
|
on 'hello' do
|
8
8
|
on :id do
|
9
9
|
get do
|
10
|
-
res.json
|
10
|
+
res.json({ hello: 'world', id: inbox[:id] })
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
14
|
get do
|
15
|
-
res.json
|
15
|
+
res.json([{ hello: 'world' }])
|
16
16
|
end
|
17
17
|
|
18
18
|
post do
|
19
19
|
res.status = 201
|
20
|
-
res.json
|
20
|
+
res.json({ hello: 'world' })
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
data/benchmarks/benchmarks.rb
CHANGED
@@ -19,28 +19,25 @@ apps = Dir['./apps/*.ru'].each_with_object({}) do |config, hash|
|
|
19
19
|
end
|
20
20
|
apps.freeze
|
21
21
|
|
22
|
+
bench = lambda do |app|
|
23
|
+
examples.each do |example|
|
24
|
+
env, expected_status = example
|
25
|
+
100.times { app.call(env) }
|
26
|
+
response = app.call(env)
|
27
|
+
raise unless response[0] == expected_status
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
22
31
|
Benchmark.ips do |x|
|
23
32
|
apps.each do |config, app|
|
24
|
-
x.report(config)
|
25
|
-
examples.each do |example|
|
26
|
-
env, expected_status = example
|
27
|
-
response = app.call(env)
|
28
|
-
raise unless response[0] == expected_status
|
29
|
-
end
|
30
|
-
end
|
33
|
+
x.report(config) { bench.call(app) }
|
31
34
|
end
|
32
35
|
x.compare!
|
33
36
|
end
|
34
37
|
|
35
38
|
Benchmark.memory do |x|
|
36
39
|
apps.each do |config, app|
|
37
|
-
x.report(config)
|
38
|
-
examples.each do |example|
|
39
|
-
env, expected_status = example
|
40
|
-
response = app.call(env)
|
41
|
-
raise unless response[0] == expected_status
|
42
|
-
end
|
43
|
-
end
|
40
|
+
x.report(config) { bench.call(app) }
|
44
41
|
end
|
45
42
|
x.compare!
|
46
43
|
end
|
data/lib/openapi_first/app.rb
CHANGED
@@ -11,17 +11,15 @@ module OpenapiFirst
|
|
11
11
|
namespace:,
|
12
12
|
router_raise_error: false,
|
13
13
|
request_validation_raise_error: false,
|
14
|
-
response_validation: false
|
14
|
+
response_validation: false,
|
15
|
+
resolver: nil
|
15
16
|
)
|
16
17
|
@stack = Rack::Builder.app do
|
17
18
|
freeze_app
|
18
19
|
use OpenapiFirst::Router, spec: spec, raise_error: router_raise_error, parent_app: parent_app
|
19
20
|
use OpenapiFirst::RequestValidation, raise_error: request_validation_raise_error
|
20
21
|
use OpenapiFirst::ResponseValidation if response_validation
|
21
|
-
run OpenapiFirst::Responder.new(
|
22
|
-
spec: spec,
|
23
|
-
namespace: namespace
|
24
|
-
)
|
22
|
+
run OpenapiFirst::Responder.new(namespace: namespace, resolver: resolver)
|
25
23
|
end
|
26
24
|
end
|
27
25
|
|
@@ -3,20 +3,14 @@
|
|
3
3
|
require_relative 'utils'
|
4
4
|
|
5
5
|
module OpenapiFirst
|
6
|
-
class
|
7
|
-
def initialize(
|
6
|
+
class DefaultOperationResolver
|
7
|
+
def initialize(namespace)
|
8
8
|
@namespace = namespace
|
9
|
-
@handlers =
|
10
|
-
operation_id = operation.operation_id
|
11
|
-
handler = find_handler(operation_id)
|
12
|
-
next if handler.nil?
|
13
|
-
|
14
|
-
hash[operation_id] = handler
|
15
|
-
end
|
9
|
+
@handlers = {}
|
16
10
|
end
|
17
11
|
|
18
|
-
def
|
19
|
-
@handlers[
|
12
|
+
def call(operation)
|
13
|
+
@handlers[operation.name] ||= find_handler(operation['x-handler'] || operation['operationId'])
|
20
14
|
end
|
21
15
|
|
22
16
|
def find_handler(operation_id)
|
@@ -4,15 +4,16 @@ require_relative 'operation'
|
|
4
4
|
|
5
5
|
module OpenapiFirst
|
6
6
|
class Definition
|
7
|
-
attr_reader :filepath
|
7
|
+
attr_reader :filepath, :operations
|
8
8
|
|
9
|
-
def initialize(
|
10
|
-
@filepath =
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
9
|
+
def initialize(resolved, filepath)
|
10
|
+
@filepath = filepath
|
11
|
+
methods = %w[get head post put patch delete trace options]
|
12
|
+
@operations = resolved['paths'].flat_map do |path, path_item|
|
13
|
+
path_item.slice(*methods).map do |request_method, _operation_object|
|
14
|
+
Operation.new(path, request_method, path_item)
|
15
|
+
end
|
16
|
+
end
|
16
17
|
end
|
17
18
|
end
|
18
19
|
end
|
@@ -2,32 +2,49 @@
|
|
2
2
|
|
3
3
|
require 'forwardable'
|
4
4
|
require 'json_schemer'
|
5
|
+
require_relative 'schema_validation'
|
5
6
|
require_relative 'utils'
|
6
7
|
require_relative 'response_object'
|
7
8
|
|
8
9
|
module OpenapiFirst
|
9
|
-
class Operation
|
10
|
+
class Operation # rubocop:disable Metrics/ClassLength
|
10
11
|
extend Forwardable
|
11
|
-
def_delegators
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:request_body,
|
15
|
-
:operation_id
|
12
|
+
def_delegators :operation_object,
|
13
|
+
:[],
|
14
|
+
:dig
|
16
15
|
|
17
|
-
|
18
|
-
|
16
|
+
WRITE_METHODS = Set.new(%w[post put patch delete]).freeze
|
17
|
+
private_constant :WRITE_METHODS
|
18
|
+
|
19
|
+
attr_reader :path, :method
|
20
|
+
|
21
|
+
def initialize(path, request_method, path_item_object)
|
22
|
+
@path = path
|
23
|
+
@method = request_method
|
24
|
+
@path_item_object = path_item_object
|
25
|
+
end
|
26
|
+
|
27
|
+
def operation_id
|
28
|
+
operation_object['operationId']
|
19
29
|
end
|
20
30
|
|
21
|
-
def
|
22
|
-
|
31
|
+
def read?
|
32
|
+
!write?
|
23
33
|
end
|
24
34
|
|
25
|
-
def
|
26
|
-
|
35
|
+
def write?
|
36
|
+
WRITE_METHODS.include?(method)
|
37
|
+
end
|
38
|
+
|
39
|
+
def request_body
|
40
|
+
operation_object['requestBody']
|
27
41
|
end
|
28
42
|
|
29
43
|
def parameters_schema
|
30
|
-
@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
|
31
48
|
end
|
32
49
|
|
33
50
|
def content_type_for(status)
|
@@ -42,22 +59,28 @@ module OpenapiFirst
|
|
42
59
|
raise ResponseInvalid, "Response has no content-type for '#{name}'" unless content_type
|
43
60
|
|
44
61
|
media_type = find_content_for_content_type(content, content_type)
|
62
|
+
|
45
63
|
unless media_type
|
46
64
|
message = "Response content type not found '#{content_type}' for '#{name}'"
|
47
65
|
raise ResponseContentTypeNotFoundError, message
|
48
66
|
end
|
49
|
-
media_type['schema']
|
67
|
+
schema = media_type['schema']
|
68
|
+
SchemaValidation.new(schema, write: false) if schema
|
50
69
|
end
|
51
70
|
|
52
|
-
def
|
53
|
-
content =
|
71
|
+
def request_body_schema(request_content_type)
|
72
|
+
content = operation_object.dig('requestBody', 'content')
|
54
73
|
media_type = find_content_for_content_type(content, request_content_type)
|
55
|
-
media_type&.fetch('schema', nil)
|
74
|
+
schema = media_type&.fetch('schema', nil)
|
75
|
+
return unless schema
|
76
|
+
|
77
|
+
SchemaValidation.new(schema, write: write?)
|
56
78
|
end
|
57
79
|
|
58
80
|
def response_for(status)
|
59
|
-
|
60
|
-
|
81
|
+
response_content = response_by_code(status)
|
82
|
+
return response_content if response_content
|
83
|
+
|
61
84
|
message = "Response status code or default not found: #{status} for '#{name}'"
|
62
85
|
raise OpenapiFirst::ResponseCodeNotFoundError, message
|
63
86
|
end
|
@@ -68,6 +91,15 @@ module OpenapiFirst
|
|
68
91
|
|
69
92
|
private
|
70
93
|
|
94
|
+
def response_by_code(status)
|
95
|
+
operation_object.dig('responses', status.to_s) ||
|
96
|
+
operation_object.dig('responses', 'default')
|
97
|
+
end
|
98
|
+
|
99
|
+
def operation_object
|
100
|
+
@path_item_object[method]
|
101
|
+
end
|
102
|
+
|
71
103
|
def find_content_for_content_type(content, request_content_type)
|
72
104
|
content.fetch(request_content_type) do |_|
|
73
105
|
type = request_content_type.split(';')[0]
|
@@ -76,24 +108,32 @@ module OpenapiFirst
|
|
76
108
|
end
|
77
109
|
|
78
110
|
def build_parameters_json_schema
|
79
|
-
|
111
|
+
parameters = all_parameters
|
112
|
+
return unless parameters&.any?
|
80
113
|
|
81
|
-
|
82
|
-
params = Rack::Utils.parse_nested_query(parameter
|
114
|
+
parameters.each_with_object(new_node) do |parameter, schema|
|
115
|
+
params = Rack::Utils.parse_nested_query(parameter['name'])
|
83
116
|
generate_schema(schema, params, parameter)
|
84
117
|
end
|
85
118
|
end
|
86
119
|
|
87
|
-
def
|
120
|
+
def all_parameters
|
121
|
+
parameters = @path_item_object['parameters']&.dup || []
|
122
|
+
parameters_on_operation = operation_object['parameters']
|
123
|
+
parameters.concat(parameters_on_operation) if parameters_on_operation
|
124
|
+
parameters
|
125
|
+
end
|
126
|
+
|
127
|
+
def generate_schema(schema, params, parameter)
|
88
128
|
required = Set.new(schema['required'])
|
89
129
|
params.each do |key, value|
|
90
|
-
required << key if parameter
|
130
|
+
required << key if parameter['required']
|
91
131
|
if value.is_a? Hash
|
92
132
|
property_schema = new_node
|
93
133
|
generate_schema(property_schema, value, parameter)
|
94
134
|
Utils.deep_merge!(schema['properties'], { key => property_schema })
|
95
135
|
else
|
96
|
-
schema['properties'][key] = parameter
|
136
|
+
schema['properties'][key] = parameter['schema']
|
97
137
|
end
|
98
138
|
end
|
99
139
|
schema['required'] = required.to_a
|
@@ -16,7 +16,7 @@ module OpenapiFirst
|
|
16
16
|
@raise = raise_error
|
17
17
|
end
|
18
18
|
|
19
|
-
def call(env) # rubocop:disable Metrics/AbcSize
|
19
|
+
def call(env) # rubocop:disable Metrics/AbcSize
|
20
20
|
operation = env[OpenapiFirst::OPERATION]
|
21
21
|
return @app.call(env) unless operation
|
22
22
|
|
@@ -45,17 +45,17 @@ module OpenapiFirst
|
|
45
45
|
validate_request_body_presence!(body, operation)
|
46
46
|
return if body.empty?
|
47
47
|
|
48
|
-
schema = request_body_schema(content_type
|
48
|
+
schema = operation&.request_body_schema(content_type)
|
49
49
|
return unless schema
|
50
50
|
|
51
51
|
parsed_request_body = parse_request_body!(body)
|
52
|
-
errors =
|
52
|
+
errors = schema.validate(parsed_request_body)
|
53
53
|
halt_with_error(400, serialize_request_body_errors(errors)) if errors.any?
|
54
|
-
env[INBOX].merge! env[REQUEST_BODY] = parsed_request_body
|
54
|
+
env[INBOX].merge! env[REQUEST_BODY] = Utils.deep_symbolize(parsed_request_body)
|
55
55
|
end
|
56
56
|
|
57
57
|
def parse_request_body!(body)
|
58
|
-
MultiJson.load(body
|
58
|
+
MultiJson.load(body)
|
59
59
|
rescue MultiJson::ParseError => e
|
60
60
|
err = { title: 'Failed to parse body as JSON' }
|
61
61
|
err[:detail] = e.cause unless ENV['RACK_ENV'] == 'production'
|
@@ -63,21 +63,17 @@ module OpenapiFirst
|
|
63
63
|
end
|
64
64
|
|
65
65
|
def validate_request_content_type!(content_type, operation)
|
66
|
-
return if operation.request_body.content
|
66
|
+
return if operation.request_body.dig('content', content_type)
|
67
67
|
|
68
68
|
halt_with_error(415)
|
69
69
|
end
|
70
70
|
|
71
71
|
def validate_request_body_presence!(body, operation)
|
72
|
-
return unless operation.request_body
|
72
|
+
return unless operation.request_body['required'] && body.empty?
|
73
73
|
|
74
74
|
halt_with_error(415, 'Request body is required')
|
75
75
|
end
|
76
76
|
|
77
|
-
def validate_json_schema(schema, object)
|
78
|
-
schema.validate(Utils.deep_stringify(object))
|
79
|
-
end
|
80
|
-
|
81
77
|
def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
|
82
78
|
{
|
83
79
|
status: status.to_s,
|
@@ -95,14 +91,6 @@ module OpenapiFirst
|
|
95
91
|
).finish
|
96
92
|
end
|
97
93
|
|
98
|
-
def request_body_schema(content_type, operation)
|
99
|
-
return unless operation
|
100
|
-
|
101
|
-
schema = operation.request_body_schema_for(content_type)
|
102
|
-
|
103
|
-
JSONSchemer.schema(schema) if schema
|
104
|
-
end
|
105
|
-
|
106
94
|
def serialize_request_body_errors(validation_errors)
|
107
95
|
validation_errors.map do |error|
|
108
96
|
{
|
@@ -114,14 +102,11 @@ module OpenapiFirst
|
|
114
102
|
end
|
115
103
|
|
116
104
|
def validate_query_parameters!(env, operation, params)
|
117
|
-
|
118
|
-
return unless
|
119
|
-
|
120
|
-
params = filtered_params(
|
121
|
-
errors =
|
122
|
-
operation.parameters_schema,
|
123
|
-
params
|
124
|
-
)
|
105
|
+
schema = operation.parameters_schema
|
106
|
+
return unless schema
|
107
|
+
|
108
|
+
params = filtered_params(schema.raw_schema, params)
|
109
|
+
errors = schema.validate(Utils.deep_stringify(params))
|
125
110
|
halt_with_error(400, serialize_query_parameter_errors(errors)) if errors.any?
|
126
111
|
env[PARAMETERS] = params
|
127
112
|
env[INBOX].merge! params
|
@@ -157,6 +142,8 @@ module OpenapiFirst
|
|
157
142
|
end
|
158
143
|
|
159
144
|
def parse_array_parameter(value, schema)
|
145
|
+
return value if value.nil? || value.empty?
|
146
|
+
|
160
147
|
array = value.is_a?(Array) ? value : value.split(',')
|
161
148
|
return array unless schema['items']
|
162
149
|
|
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
require 'rack'
|
4
4
|
require_relative 'inbox'
|
5
|
-
require_relative '
|
5
|
+
require_relative 'default_operation_resolver'
|
6
6
|
|
7
7
|
module OpenapiFirst
|
8
8
|
class Responder
|
9
|
-
def initialize(
|
10
|
-
@resolver = resolver
|
9
|
+
def initialize(namespace: nil, resolver: nil)
|
10
|
+
@resolver = resolver || DefaultOperationResolver.new(namespace)
|
11
11
|
@namespace = namespace
|
12
12
|
end
|
13
13
|
|
@@ -24,7 +24,7 @@ module OpenapiFirst
|
|
24
24
|
private
|
25
25
|
|
26
26
|
def find_handler(operation)
|
27
|
-
handler = @resolver
|
27
|
+
handler = @resolver.call(operation)
|
28
28
|
raise NotImplementedError, "Could not find handler for #{operation.name}" unless handler
|
29
29
|
|
30
30
|
handler
|
@@ -41,7 +41,8 @@ module OpenapiFirst
|
|
41
41
|
full_body = +''
|
42
42
|
response.each { |chunk| full_body << chunk }
|
43
43
|
data = full_body.empty? ? {} : load_json(full_body)
|
44
|
-
errors =
|
44
|
+
errors = schema.validate(data)
|
45
|
+
errors = errors.to_a.map! do |error|
|
45
46
|
error_message_for(error)
|
46
47
|
end
|
47
48
|
raise ResponseBodyInvalidError, errors.join(', ') if errors.any?
|
data/lib/openapi_first/router.rb
CHANGED
@@ -40,7 +40,10 @@ module OpenapiFirst
|
|
40
40
|
|
41
41
|
def raise_error(env)
|
42
42
|
req = Rack::Request.new(env)
|
43
|
-
msg =
|
43
|
+
msg =
|
44
|
+
"Could not find definition for #{req.request_method} '#{
|
45
|
+
req.path
|
46
|
+
}' in API description #{@filepath}"
|
44
47
|
raise NotFoundError, msg
|
45
48
|
end
|
46
49
|
|
@@ -53,13 +56,12 @@ module OpenapiFirst
|
|
53
56
|
env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
|
54
57
|
end
|
55
58
|
|
56
|
-
def build_router(operations) # rubocop:disable Metrics/AbcSize
|
57
|
-
router = Hanami::Router.new
|
59
|
+
def build_router(operations) # rubocop:disable Metrics/AbcSize
|
60
|
+
router = Hanami::Router.new
|
58
61
|
operations.each do |operation|
|
59
62
|
normalized_path = operation.path.gsub('{', ':').gsub('}', '')
|
60
63
|
if operation.operation_id.nil?
|
61
64
|
warn "operationId is missing in '#{operation.method} #{operation.path}'. I am ignoring this operation."
|
62
|
-
next
|
63
65
|
end
|
64
66
|
router.public_send(
|
65
67
|
operation.method,
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json_schemer'
|
4
|
+
|
5
|
+
module OpenapiFirst
|
6
|
+
class SchemaValidation
|
7
|
+
attr_reader :raw_schema
|
8
|
+
|
9
|
+
def initialize(schema, write: true)
|
10
|
+
@raw_schema = schema
|
11
|
+
custom_keywords = {}
|
12
|
+
custom_keywords['writeOnly'] = proc { |data| !data } unless write
|
13
|
+
custom_keywords['readOnly'] = proc { |data| !data } if write
|
14
|
+
@schemer = JSONSchemer.schema(
|
15
|
+
schema,
|
16
|
+
keywords: custom_keywords,
|
17
|
+
before_property_validation: proc do |data, property, property_schema, parent|
|
18
|
+
convert_nullable(data, property, property_schema, parent)
|
19
|
+
end
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate(input)
|
24
|
+
@schemer.validate(input)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def convert_nullable(_data, _property, property_schema, _parent)
|
30
|
+
return unless property_schema.is_a?(Hash) && property_schema['nullable'] && property_schema['type']
|
31
|
+
|
32
|
+
property_schema['type'] = [*property_schema['type'], 'null']
|
33
|
+
property_schema.delete('nullable')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/openapi_first/utils.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'hanami/utils/string'
|
4
|
+
require 'hanami/utils/hash'
|
4
5
|
require 'deep_merge/core'
|
5
6
|
|
6
7
|
module OpenapiFirst
|
@@ -17,20 +18,12 @@ module OpenapiFirst
|
|
17
18
|
Hanami::Utils::String.classify(string)
|
18
19
|
end
|
19
20
|
|
20
|
-
def self.
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
when Array
|
27
|
-
value.map do |item|
|
28
|
-
item.is_a?(::Hash) ? deep_stringify(item) : item
|
29
|
-
end
|
30
|
-
else
|
31
|
-
value
|
32
|
-
end
|
33
|
-
end
|
21
|
+
def self.deep_symbolize(hash)
|
22
|
+
Hanami::Utils::Hash.deep_symbolize(hash)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.deep_stringify(hash)
|
26
|
+
Hanami::Utils::Hash.deep_stringify(hash)
|
34
27
|
end
|
35
28
|
end
|
36
29
|
end
|
@@ -6,17 +6,37 @@ module OpenapiFirst
|
|
6
6
|
|
7
7
|
# rubocop:disable Metrics/MethodLength
|
8
8
|
# rubocop:disable Metrics/AbcSize
|
9
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
10
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
9
11
|
def self.error_details(error)
|
10
12
|
if error['type'] == 'pattern'
|
11
13
|
{
|
12
14
|
title: 'is not valid',
|
13
15
|
detail: "does not match pattern '#{error['schema']['pattern']}'"
|
14
16
|
}
|
17
|
+
elsif error['type'] == 'format'
|
18
|
+
{
|
19
|
+
title: "has not a valid #{error.dig('schema', 'format')} format",
|
20
|
+
detail: "#{error['data'].inspect} is not a valid #{error.dig('schema', 'format')} format"
|
21
|
+
}
|
22
|
+
elsif error['type'] == 'enum'
|
23
|
+
{
|
24
|
+
title: "value #{error['data'].inspect} is not defined in enum",
|
25
|
+
detail: "value can be one of #{error.dig('schema', 'enum')&.join(', ')}"
|
26
|
+
}
|
15
27
|
elsif error['type'] == 'required'
|
16
28
|
missing_keys = error['details']['missing_keys']
|
17
29
|
{
|
18
30
|
title: "is missing required properties: #{missing_keys.join(', ')}"
|
19
31
|
}
|
32
|
+
elsif error['type'] == 'readOnly'
|
33
|
+
{
|
34
|
+
title: 'appears in request, but is read-only'
|
35
|
+
}
|
36
|
+
elsif error['type'] == 'writeOnly'
|
37
|
+
{
|
38
|
+
title: 'write-only field appears in response:'
|
39
|
+
}
|
20
40
|
elsif SIMPLE_TYPES.include?(error['type'])
|
21
41
|
{
|
22
42
|
title: "should be a #{error['type']}"
|
@@ -29,5 +49,7 @@ module OpenapiFirst
|
|
29
49
|
end
|
30
50
|
# rubocop:enable Metrics/MethodLength
|
31
51
|
# rubocop:enable Metrics/AbcSize
|
52
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
53
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
32
54
|
end
|
33
55
|
end
|
data/lib/openapi_first.rb
CHANGED
@@ -25,10 +25,9 @@ module OpenapiFirst
|
|
25
25
|
|
26
26
|
def self.load(spec_path, only: nil)
|
27
27
|
content = YAML.load_file(spec_path)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
Definition.new(parsed)
|
28
|
+
resolved = OasParser::Parser.new(spec_path, content).resolve
|
29
|
+
resolved['paths'].filter!(&->(key, _) { only.call(key) }) if only
|
30
|
+
Definition.new(resolved, spec_path)
|
32
31
|
end
|
33
32
|
|
34
33
|
def self.app(
|
@@ -78,11 +77,17 @@ module OpenapiFirst
|
|
78
77
|
end
|
79
78
|
|
80
79
|
class Error < StandardError; end
|
80
|
+
|
81
81
|
class NotFoundError < Error; end
|
82
|
+
|
82
83
|
class NotImplementedError < RuntimeError; end
|
84
|
+
|
83
85
|
class ResponseInvalid < Error; end
|
86
|
+
|
84
87
|
class ResponseCodeNotFoundError < ResponseInvalid; end
|
88
|
+
|
85
89
|
class ResponseContentTypeNotFoundError < ResponseInvalid; end
|
90
|
+
|
86
91
|
class ResponseBodyInvalidError < ResponseInvalid; end
|
87
92
|
|
88
93
|
class RequestInvalidError < Error
|
data/openapi_first.gemspec
CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.metadata['changelog_uri'] = 'https://github.com/ahx/openapi_first/blob/master/CHANGELOG.md'
|
21
21
|
else
|
22
22
|
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
23
|
-
|
23
|
+
'public gem pushes.'
|
24
24
|
end
|
25
25
|
|
26
26
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
@@ -32,10 +32,12 @@ Gem::Specification.new do |spec|
|
|
32
32
|
spec.bindir = 'exe'
|
33
33
|
spec.require_paths = ['lib']
|
34
34
|
|
35
|
+
spec.required_ruby_version = '>= 2.6.0'
|
36
|
+
|
35
37
|
spec.add_runtime_dependency 'deep_merge', '>= 1.2.1'
|
36
38
|
spec.add_runtime_dependency 'hanami-router', '~> 2.0.alpha3'
|
37
39
|
spec.add_runtime_dependency 'hanami-utils', '~> 2.0.alpha1'
|
38
|
-
spec.add_runtime_dependency 'json_schemer', '~> 0.2'
|
40
|
+
spec.add_runtime_dependency 'json_schemer', '~> 0.2.16'
|
39
41
|
spec.add_runtime_dependency 'multi_json', '~> 1.14'
|
40
42
|
spec.add_runtime_dependency 'oas_parser', '~> 0.25.1'
|
41
43
|
spec.add_runtime_dependency 'rack', '~> 2.2'
|