openapi_first 0.10.1 → 0.12.0.alpha2
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 +12 -0
- data/CHANGELOG.md +22 -0
- data/Gemfile.lock +15 -13
- data/README.md +126 -83
- data/benchmarks/Gemfile.lock +11 -11
- data/examples/app.rb +6 -1
- data/lib/openapi_first.rb +46 -5
- data/lib/openapi_first/app.rb +9 -11
- data/lib/openapi_first/definition.rb +3 -12
- data/lib/openapi_first/find_handler.rb +60 -0
- data/lib/openapi_first/operation.rb +23 -5
- data/lib/openapi_first/request_validation.rb +18 -8
- data/lib/openapi_first/responder.rb +46 -0
- data/lib/openapi_first/response_object.rb +21 -0
- data/lib/openapi_first/response_validation.rb +67 -0
- data/lib/openapi_first/response_validator.rb +17 -10
- data/lib/openapi_first/router.rb +35 -47
- data/lib/openapi_first/router_required.rb +13 -0
- data/lib/openapi_first/validation_format.rb +3 -1
- data/lib/openapi_first/version.rb +1 -1
- data/openapi_first.gemspec +7 -7
- metadata +11 -7
- data/lib/openapi_first/operation_resolver.rb +0 -27
@@ -3,30 +3,37 @@
|
|
3
3
|
require 'json_schemer'
|
4
4
|
require 'multi_json'
|
5
5
|
require_relative 'validation'
|
6
|
+
require_relative 'router'
|
6
7
|
|
7
8
|
module OpenapiFirst
|
8
9
|
class ResponseValidator
|
9
10
|
def initialize(spec)
|
10
11
|
@spec = spec
|
12
|
+
@router = Router.new(->(_env) {}, spec: spec, raise_error: true)
|
11
13
|
end
|
12
14
|
|
13
15
|
def validate(request, response)
|
14
16
|
errors = validation_errors(request, response)
|
15
17
|
Validation.new(errors || [])
|
16
|
-
rescue
|
18
|
+
rescue OpenapiFirst::ResponseCodeNotFoundError, OpenapiFirst::NotFoundError => e
|
19
|
+
Validation.new([e.message])
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate_operation(request, response)
|
23
|
+
errors = validation_errors(request, response)
|
24
|
+
Validation.new(errors || [])
|
25
|
+
rescue OpenapiFirst::ResponseCodeNotFoundError, OpenapiFirst::NotFoundError => e
|
17
26
|
Validation.new([e.message])
|
18
27
|
end
|
19
28
|
|
20
29
|
private
|
21
30
|
|
22
31
|
def validation_errors(request, response)
|
23
|
-
content = response_for(request, response)&.content
|
32
|
+
content = response_for(request, response)&.fetch('content', nil)
|
24
33
|
return unless content
|
25
34
|
|
26
35
|
content_type = content[response.content_type]
|
27
|
-
unless content_type
|
28
|
-
return ["Content type not found: '#{response.content_type}'"]
|
29
|
-
end
|
36
|
+
return ["Content type not found: '#{response.content_type}'"] unless content_type
|
30
37
|
|
31
38
|
response_schema = content_type['schema']
|
32
39
|
return unless response_schema
|
@@ -46,14 +53,14 @@ module OpenapiFirst
|
|
46
53
|
.merge!(
|
47
54
|
data_pointer: error['data_pointer'],
|
48
55
|
schema_pointer: error['schema_pointer']
|
49
|
-
)
|
50
|
-
end
|
56
|
+
)
|
51
57
|
end
|
52
58
|
|
53
59
|
def response_for(request, response)
|
54
|
-
|
55
|
-
|
56
|
-
|
60
|
+
env = request.env.dup
|
61
|
+
@router.call(env)
|
62
|
+
operation = env[OPERATION]
|
63
|
+
operation&.response_for(response.status)
|
57
64
|
end
|
58
65
|
end
|
59
66
|
end
|
data/lib/openapi_first/router.rb
CHANGED
@@ -7,76 +7,65 @@ require_relative 'utils'
|
|
7
7
|
module OpenapiFirst
|
8
8
|
class Router
|
9
9
|
NOT_FOUND = Rack::Response.new('', 404).finish.freeze
|
10
|
+
DEFAULT_NOT_FOUND_APP = ->(_env) { NOT_FOUND }
|
10
11
|
|
11
|
-
def initialize(
|
12
|
+
def initialize(
|
13
|
+
app,
|
14
|
+
spec:,
|
15
|
+
raise_error: false,
|
16
|
+
parent_app: nil,
|
17
|
+
not_found: nil
|
18
|
+
)
|
12
19
|
@app = app
|
13
|
-
@
|
14
|
-
@
|
15
|
-
@
|
20
|
+
@parent_app = parent_app
|
21
|
+
@raise = raise_error
|
22
|
+
@failure_app = find_failure_app(not_found)
|
23
|
+
if @failure_app.nil?
|
24
|
+
raise ArgumentError,
|
25
|
+
'not_found must be nil, :continue or must respond to call'
|
26
|
+
end
|
27
|
+
@filepath = spec.filepath
|
28
|
+
@router = build_router(spec.operations)
|
16
29
|
end
|
17
30
|
|
18
31
|
def call(env)
|
19
|
-
|
20
|
-
|
32
|
+
env[OPERATION] = nil
|
33
|
+
route = find_route(env)
|
34
|
+
return route.call(env) if route.routable?
|
35
|
+
|
36
|
+
if @raise
|
37
|
+
req = Rack::Request.new(env)
|
38
|
+
msg = "Could not find definition for #{req.request_method} '#{req.path}' in API description #{@filepath}"
|
39
|
+
raise NotFoundError, msg
|
40
|
+
end
|
21
41
|
return @parent_app.call(env) if @parent_app
|
22
42
|
|
23
|
-
|
43
|
+
@failure_app.call(env)
|
24
44
|
end
|
25
45
|
|
26
|
-
|
27
|
-
name = operation_id.match(/:*(.*)/)&.to_a&.at(1)
|
28
|
-
return if name.nil?
|
29
|
-
|
30
|
-
if name.include?('.')
|
31
|
-
module_name, method_name = name.split('.')
|
32
|
-
klass = find_const(@namespace, module_name)
|
33
|
-
return klass&.method(Utils.underscore(method_name))
|
34
|
-
end
|
35
|
-
if name.include?('#')
|
36
|
-
module_name, klass_name = name.split('#')
|
37
|
-
const = find_const(@namespace, module_name)
|
38
|
-
klass = find_const(const, klass_name)
|
39
|
-
if klass.instance_method(:initialize).arity.zero?
|
40
|
-
return ->(params, res) { klass.new.call(params, res) }
|
41
|
-
end
|
46
|
+
private
|
42
47
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
return unless @namespace.respond_to?(method_name)
|
48
|
+
def find_failure_app(option)
|
49
|
+
return DEFAULT_NOT_FOUND_APP if option.nil?
|
50
|
+
return @app if option == :continue
|
47
51
|
|
48
|
-
|
52
|
+
option if option.respond_to?(:call)
|
49
53
|
end
|
50
54
|
|
51
|
-
|
52
|
-
|
53
|
-
def find_endpoint(env)
|
55
|
+
def find_route(env)
|
54
56
|
original_path_info = env[Rack::PATH_INFO]
|
55
|
-
# Overwrite PATH_INFO temporarily, because hanami-router does not respect SCRIPT_NAME # rubocop:disable Layout/LineLength
|
56
57
|
env[Rack::PATH_INFO] = Rack::Request.new(env).path
|
57
|
-
@router.recognize(env)
|
58
|
+
@router.recognize(env)
|
58
59
|
ensure
|
59
60
|
env[Rack::PATH_INFO] = original_path_info
|
60
61
|
end
|
61
62
|
|
62
|
-
def find_const(parent, name)
|
63
|
-
name = Utils.classify(name)
|
64
|
-
return unless parent.const_defined?(name, false)
|
65
|
-
|
66
|
-
parent.const_get(name, false)
|
67
|
-
end
|
68
|
-
|
69
63
|
def build_router(operations) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
70
64
|
router = Hanami::Router.new {}
|
71
65
|
operations.each do |operation|
|
72
66
|
normalized_path = operation.path.gsub('{', ':').gsub('}', '')
|
73
67
|
if operation.operation_id.nil?
|
74
|
-
warn "operationId is missing in '#{operation.method} #{operation.path}'. I am ignoring this operation."
|
75
|
-
next
|
76
|
-
end
|
77
|
-
handler = @namespace && find_handler(operation.operation_id)
|
78
|
-
if @namespace && handler.nil?
|
79
|
-
warn "#{self.class.name} cannot not find handler for '#{operation.operation_id}' (#{operation.method} #{operation.path}). This operation will be ignored." # rubocop:disable Layout/LineLength
|
68
|
+
warn "operationId is missing in '#{operation.method} #{operation.path}'. I am ignoring this operation."
|
80
69
|
next
|
81
70
|
end
|
82
71
|
router.public_send(
|
@@ -85,7 +74,6 @@ module OpenapiFirst
|
|
85
74
|
to: lambda do |env|
|
86
75
|
env[OPERATION] = operation
|
87
76
|
env[PARAMETERS] = Utils.deep_stringify(env['router.params'])
|
88
|
-
env[HANDLER] = handler
|
89
77
|
@app.call(env)
|
90
78
|
end
|
91
79
|
)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
module RouterRequired
|
5
|
+
def call(env)
|
6
|
+
unless env.key?(OPERATION)
|
7
|
+
raise 'OpenapiFirst::Router missing in middleware stack. Did you forget adding OpenapiFirst::Router?'
|
8
|
+
end
|
9
|
+
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -5,6 +5,7 @@ module OpenapiFirst
|
|
5
5
|
SIMPLE_TYPES = %w[string integer].freeze
|
6
6
|
|
7
7
|
# rubocop:disable Metrics/MethodLength
|
8
|
+
# rubocop:disable Metrics/AbcSize
|
8
9
|
def self.error_details(error)
|
9
10
|
if error['type'] == 'pattern'
|
10
11
|
{
|
@@ -23,9 +24,10 @@ module OpenapiFirst
|
|
23
24
|
elsif error['schema'] == false
|
24
25
|
{ title: 'unknown fields are not allowed' }
|
25
26
|
else
|
26
|
-
{ title:
|
27
|
+
{ title: "is not valid: #{error['data'].inspect}" }
|
27
28
|
end
|
28
29
|
end
|
29
30
|
# rubocop:enable Metrics/MethodLength
|
31
|
+
# rubocop:enable Metrics/AbcSize
|
30
32
|
end
|
31
33
|
end
|
data/openapi_first.gemspec
CHANGED
@@ -32,13 +32,13 @@ Gem::Specification.new do |spec|
|
|
32
32
|
spec.bindir = 'exe'
|
33
33
|
spec.require_paths = ['lib']
|
34
34
|
|
35
|
-
spec.
|
36
|
-
spec.
|
37
|
-
spec.
|
38
|
-
spec.
|
39
|
-
spec.
|
40
|
-
spec.
|
41
|
-
spec.
|
35
|
+
spec.add_runtime_dependency 'deep_merge', '>= 1.2.1'
|
36
|
+
spec.add_runtime_dependency 'hanami-router', '~> 2.0.alpha3'
|
37
|
+
spec.add_runtime_dependency 'hanami-utils', '~> 2.0.alpha1'
|
38
|
+
spec.add_runtime_dependency 'json_schemer', '~> 0.2'
|
39
|
+
spec.add_runtime_dependency 'multi_json', '~> 1.14'
|
40
|
+
spec.add_runtime_dependency 'oas_parser', '~> 0.25.1'
|
41
|
+
spec.add_runtime_dependency 'rack', '~> 2.2'
|
42
42
|
|
43
43
|
spec.add_development_dependency 'bundler', '~> 2'
|
44
44
|
spec.add_development_dependency 'rack-test', '~> 1'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: openapi_first
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.12.0.alpha2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andreas Haller
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-06-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deep_merge
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 2.0.
|
33
|
+
version: 2.0.alpha3
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 2.0.
|
40
|
+
version: 2.0.alpha3
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: hanami-utils
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -203,12 +203,16 @@ files:
|
|
203
203
|
- lib/openapi_first/app.rb
|
204
204
|
- lib/openapi_first/coverage.rb
|
205
205
|
- lib/openapi_first/definition.rb
|
206
|
+
- lib/openapi_first/find_handler.rb
|
206
207
|
- lib/openapi_first/inbox.rb
|
207
208
|
- lib/openapi_first/operation.rb
|
208
|
-
- lib/openapi_first/operation_resolver.rb
|
209
209
|
- lib/openapi_first/request_validation.rb
|
210
|
+
- lib/openapi_first/responder.rb
|
211
|
+
- lib/openapi_first/response_object.rb
|
212
|
+
- lib/openapi_first/response_validation.rb
|
210
213
|
- lib/openapi_first/response_validator.rb
|
211
214
|
- lib/openapi_first/router.rb
|
215
|
+
- lib/openapi_first/router_required.rb
|
212
216
|
- lib/openapi_first/utils.rb
|
213
217
|
- lib/openapi_first/validation.rb
|
214
218
|
- lib/openapi_first/validation_format.rb
|
@@ -232,9 +236,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
232
236
|
version: '0'
|
233
237
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
234
238
|
requirements:
|
235
|
-
- - "
|
239
|
+
- - ">"
|
236
240
|
- !ruby/object:Gem::Version
|
237
|
-
version:
|
241
|
+
version: 1.3.1
|
238
242
|
requirements: []
|
239
243
|
rubygems_version: 3.1.2
|
240
244
|
signing_key:
|
@@ -1,27 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rack'
|
4
|
-
require_relative 'inbox'
|
5
|
-
|
6
|
-
module OpenapiFirst
|
7
|
-
class OperationResolver
|
8
|
-
def call(env)
|
9
|
-
operation = env[OpenapiFirst::OPERATION]
|
10
|
-
res = Rack::Response.new
|
11
|
-
inbox = env[INBOX]
|
12
|
-
handler = env[HANDLER]
|
13
|
-
result = handler.call(inbox, res)
|
14
|
-
res.write serialize(result) if result && res.body.empty?
|
15
|
-
res[Rack::CONTENT_TYPE] ||= operation.content_type_for(res.status)
|
16
|
-
res.finish
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
def serialize(result)
|
22
|
-
return result if result.is_a?(String)
|
23
|
-
|
24
|
-
MultiJson.dump(result)
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|