openapi_first 0.12.0.alpha1 → 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/CHANGELOG.md +5 -1
- data/Gemfile.lock +2 -2
- data/README.md +6 -4
- data/benchmarks/Gemfile.lock +2 -2
- data/examples/app.rb +6 -1
- data/lib/openapi_first.rb +2 -1
- data/lib/openapi_first/app.rb +1 -0
- data/lib/openapi_first/find_handler.rb +25 -21
- data/lib/openapi_first/operation.rb +6 -6
- data/lib/openapi_first/responder.rb +10 -3
- data/lib/openapi_first/response_validation.rb +8 -2
- data/lib/openapi_first/validation_format.rb +3 -1
- data/lib/openapi_first/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4d0848ff82fa49a93053ec107ca28c419c1be8b9122301a8819742685e8b7995
|
4
|
+
data.tar.gz: 11cf9a3c60b619cd55d7f3f552a2c367553aa69ed047ddd101eff1b460ee4c53
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5cabc3434dd9c4801b3f6ec030ec4ad5ef6905762a2643ef59a99f78057e6b108f90bf24696d29cab776dfa61a571be4973af7f62af07f5672f64996fb77086d
|
7
|
+
data.tar.gz: b7391c86d982be35d680e56f904261b8cfbd846a8206c2f339adc703248ad2476121fb8534a9562a20e081a2e5d5a8696231937affb23a72fe917a546a7e34c2
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
## Unreleased
|
3
|
+
## Unreleased
|
4
|
+
- Add `raise_error` option to OpenapiFirst.app (false by default)
|
5
|
+
- Add ResponseValidation to OpenapiFirst.app if raise_error option is true
|
4
6
|
- Rename `raise` option to `raise_error`
|
5
7
|
- Add `raise_error` option to RequestValidation middleware
|
8
|
+
- Raise error if handler could not be found by Responder
|
9
|
+
- Add `Operation#name` that returns a human readable name for an operation
|
6
10
|
|
7
11
|
## 0.11.0
|
8
12
|
- Raise error if you forgot to add the Router middleware
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
openapi_first (0.12.0.
|
4
|
+
openapi_first (0.12.0.alpha2)
|
5
5
|
deep_merge (>= 1.2.1)
|
6
6
|
hanami-router (~> 2.0.alpha3)
|
7
7
|
hanami-utils (~> 2.0.alpha1)
|
@@ -39,7 +39,7 @@ GEM
|
|
39
39
|
transproc (~> 1.0)
|
40
40
|
hansi (0.2.0)
|
41
41
|
hash-deep-merge (0.1.1)
|
42
|
-
i18n (1.8.
|
42
|
+
i18n (1.8.3)
|
43
43
|
concurrent-ruby (~> 1.0)
|
44
44
|
json_schemer (0.2.11)
|
45
45
|
ecma-re-validator (~> 0.2)
|
data/README.md
CHANGED
@@ -8,6 +8,12 @@ Start with writing an OpenAPI file that describes the API, which you are about t
|
|
8
8
|
|
9
9
|
You can use OpenapiFirst via its [Rack middlewares](#rack-middlewares) or in [standalone mode](#standalone-usage).
|
10
10
|
|
11
|
+
## Alternatives
|
12
|
+
|
13
|
+
This gem is inspired by [committee](https://github.com/interagent/committee) (Ruby) and [connexion](https://github.com/zalando/connexion) (Python).
|
14
|
+
|
15
|
+
Here's a [comparison between committee and openapi_first](https://gist.github.com/ahx/1538c31f0652f459861713b5259e366a).
|
16
|
+
|
11
17
|
## Rack middlewares
|
12
18
|
OpenapiFirst consists of these Rack middlewares:
|
13
19
|
|
@@ -243,10 +249,6 @@ end
|
|
243
249
|
|
244
250
|
Out of scope. Use [Prism](https://github.com/stoplightio/prism) or [fakeit](https://github.com/JustinFeng/fakeit).
|
245
251
|
|
246
|
-
## Alternatives
|
247
|
-
|
248
|
-
This gem is inspired by [committee](https://github.com/interagent/committee) (Ruby) and [connexion](https://github.com/zalando/connexion) (Python).
|
249
|
-
|
250
252
|
## Development
|
251
253
|
|
252
254
|
Run `bin/setup` to install dependencies.
|
data/benchmarks/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ..
|
3
3
|
specs:
|
4
|
-
openapi_first (0.
|
4
|
+
openapi_first (0.12.0.alpha2)
|
5
5
|
deep_merge (>= 1.2.1)
|
6
6
|
hanami-router (~> 2.0.alpha3)
|
7
7
|
hanami-utils (~> 2.0.alpha1)
|
@@ -108,7 +108,7 @@ GEM
|
|
108
108
|
rack (>= 0.4)
|
109
109
|
rack-protection (2.0.8.1)
|
110
110
|
rack
|
111
|
-
regexp_parser (1.7.
|
111
|
+
regexp_parser (1.7.1)
|
112
112
|
ruby2_keywords (0.0.2)
|
113
113
|
seg (1.2.0)
|
114
114
|
sinatra (2.0.8.1)
|
data/examples/app.rb
CHANGED
@@ -13,4 +13,9 @@ module Web
|
|
13
13
|
end
|
14
14
|
|
15
15
|
oas_path = File.absolute_path('./openapi.yaml', __dir__)
|
16
|
-
|
16
|
+
pp OpenapiFirst.env == 'test'
|
17
|
+
App = OpenapiFirst.app(
|
18
|
+
oas_path,
|
19
|
+
namespace: Web,
|
20
|
+
raise_error: OpenapiFirst.env == 'test'
|
21
|
+
)
|
data/lib/openapi_first.rb
CHANGED
@@ -31,7 +31,7 @@ module OpenapiFirst
|
|
31
31
|
Definition.new(parsed)
|
32
32
|
end
|
33
33
|
|
34
|
-
def self.app(spec, namespace:, raise_error:
|
34
|
+
def self.app(spec, namespace:, raise_error: false)
|
35
35
|
spec = OpenapiFirst.load(spec) if spec.is_a?(String)
|
36
36
|
App.new(nil, spec, namespace: namespace, raise_error: raise_error)
|
37
37
|
end
|
@@ -54,6 +54,7 @@ module OpenapiFirst
|
|
54
54
|
|
55
55
|
class Error < StandardError; end
|
56
56
|
class NotFoundError < Error; end
|
57
|
+
class NotImplementedError < RuntimeError; end
|
57
58
|
class ResponseCodeNotFoundError < Error; end
|
58
59
|
class ResponseMediaTypeNotFoundError < Error; end
|
59
60
|
class ResponseBodyInvalidError < Error; end
|
data/lib/openapi_first/app.rb
CHANGED
@@ -10,6 +10,7 @@ module OpenapiFirst
|
|
10
10
|
freeze_app
|
11
11
|
use OpenapiFirst::Router, spec: spec, raise_error: raise_error, parent_app: parent_app
|
12
12
|
use OpenapiFirst::RequestValidation, raise_error: raise_error
|
13
|
+
use OpenapiFirst::ResponseValidation if raise_error
|
13
14
|
run OpenapiFirst::Responder.new(
|
14
15
|
spec: spec,
|
15
16
|
namespace: namespace
|
@@ -5,14 +5,10 @@ require_relative 'utils'
|
|
5
5
|
module OpenapiFirst
|
6
6
|
class FindHandler
|
7
7
|
def initialize(spec, namespace)
|
8
|
-
@spec = spec
|
9
8
|
@namespace = namespace
|
10
|
-
|
11
|
-
|
12
|
-
def all
|
13
|
-
@spec.operations.each_with_object({}) do |operation, hash|
|
9
|
+
@handlers = spec.operations.each_with_object({}) do |operation, hash|
|
14
10
|
operation_id = operation.operation_id
|
15
|
-
handler =
|
11
|
+
handler = find_handler(operation_id)
|
16
12
|
if handler.nil?
|
17
13
|
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
|
18
14
|
next
|
@@ -21,22 +17,17 @@ module OpenapiFirst
|
|
21
17
|
end
|
22
18
|
end
|
23
19
|
|
24
|
-
def
|
20
|
+
def [](operation_id)
|
21
|
+
@handlers[operation_id]
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_handler(operation_id)
|
25
25
|
name = operation_id.match(/:*(.*)/)&.to_a&.at(1)
|
26
26
|
return if name.nil?
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
return klass&.method(Utils.underscore(method_name))
|
32
|
-
end
|
33
|
-
if name.include?('#')
|
34
|
-
module_name, klass_name = name.split('#')
|
35
|
-
const = find_const(@namespace, module_name)
|
36
|
-
klass = find_const(const, klass_name)
|
37
|
-
return ->(params, res) { klass.new.call(params, res) } if klass.instance_method(:initialize).arity.zero?
|
38
|
-
|
39
|
-
return ->(params, res) { klass.new(params.env).call(params, res) }
|
28
|
+
catch :halt do
|
29
|
+
return find_class_method_handler(name) if name.include?('.')
|
30
|
+
return find_instance_method_handler(name) if name.include?('#')
|
40
31
|
end
|
41
32
|
method_name = Utils.underscore(name)
|
42
33
|
return unless @namespace.respond_to?(method_name)
|
@@ -44,11 +35,24 @@ module OpenapiFirst
|
|
44
35
|
@namespace.method(method_name)
|
45
36
|
end
|
46
37
|
|
47
|
-
|
38
|
+
def find_class_method_handler(name)
|
39
|
+
module_name, method_name = name.split('.')
|
40
|
+
klass = find_const(@namespace, module_name)
|
41
|
+
klass.method(Utils.underscore(method_name))
|
42
|
+
end
|
43
|
+
|
44
|
+
def find_instance_method_handler(name)
|
45
|
+
module_name, klass_name = name.split('#')
|
46
|
+
const = find_const(@namespace, module_name)
|
47
|
+
klass = find_const(const, klass_name)
|
48
|
+
return ->(params, res) { klass.new.call(params, res) } if klass.instance_method(:initialize).arity.zero?
|
49
|
+
|
50
|
+
->(params, res) { klass.new(params.env).call(params, res) }
|
51
|
+
end
|
48
52
|
|
49
53
|
def find_const(parent, name)
|
50
54
|
name = Utils.classify(name)
|
51
|
-
|
55
|
+
throw :halt unless parent.const_defined?(name, false)
|
52
56
|
|
53
57
|
parent.const_get(name, false)
|
54
58
|
end
|
@@ -36,7 +36,7 @@ module OpenapiFirst
|
|
36
36
|
|
37
37
|
media_type = content[content_type]
|
38
38
|
unless media_type
|
39
|
-
message = "Response
|
39
|
+
message = "Response content type not found: '#{content_type}' for '#{name}'"
|
40
40
|
raise ResponseMediaTypeNotFoundError, message
|
41
41
|
end
|
42
42
|
media_type['schema']
|
@@ -45,16 +45,16 @@ module OpenapiFirst
|
|
45
45
|
def response_for(status)
|
46
46
|
@operation.response_by_code(status.to_s, use_default: true).raw
|
47
47
|
rescue OasParser::ResponseCodeNotFound
|
48
|
-
message = "Response status code or default not found: #{status} for '#{
|
48
|
+
message = "Response status code or default not found: #{status} for '#{name}'"
|
49
49
|
raise OpenapiFirst::ResponseCodeNotFoundError, message
|
50
50
|
end
|
51
51
|
|
52
|
-
|
53
|
-
|
54
|
-
def operation_name
|
55
|
-
"#{method.upcase} #{path}"
|
52
|
+
def name
|
53
|
+
"#{method.upcase} #{path} (#{operation_id})"
|
56
54
|
end
|
57
55
|
|
56
|
+
private
|
57
|
+
|
58
58
|
def build_parameters_json_schema
|
59
59
|
return unless @operation.parameters&.any?
|
60
60
|
|
@@ -6,15 +6,15 @@ require_relative 'find_handler'
|
|
6
6
|
|
7
7
|
module OpenapiFirst
|
8
8
|
class Responder
|
9
|
-
def initialize(spec:, namespace:)
|
10
|
-
@
|
9
|
+
def initialize(spec:, namespace:, resolver: FindHandler.new(spec, namespace))
|
10
|
+
@resolver = resolver
|
11
11
|
@namespace = namespace
|
12
12
|
end
|
13
13
|
|
14
14
|
def call(env)
|
15
15
|
operation = env[OpenapiFirst::OPERATION]
|
16
16
|
res = Rack::Response.new
|
17
|
-
handler =
|
17
|
+
handler = find_handler(operation)
|
18
18
|
result = handler.call(env[INBOX], res)
|
19
19
|
res.write serialize(result) if result && res.body.empty?
|
20
20
|
res[Rack::CONTENT_TYPE] ||= operation.content_type_for(res.status)
|
@@ -23,6 +23,13 @@ module OpenapiFirst
|
|
23
23
|
|
24
24
|
private
|
25
25
|
|
26
|
+
def find_handler(operation)
|
27
|
+
handler = @resolver[operation.operation_id]
|
28
|
+
raise NotImplementedError, "Could not find handler for #{operation.name}" unless handler
|
29
|
+
|
30
|
+
handler
|
31
|
+
end
|
32
|
+
|
26
33
|
def serialize(result)
|
27
34
|
return result if result.is_a?(String)
|
28
35
|
|
@@ -46,16 +46,22 @@ module OpenapiFirst
|
|
46
46
|
def validate_response_body(schema, response)
|
47
47
|
full_body = +''
|
48
48
|
response.each { |chunk| full_body << chunk }
|
49
|
-
data = full_body.empty? ? {} :
|
49
|
+
data = full_body.empty? ? {} : load_json(full_body)
|
50
50
|
errors = JSONSchemer.schema(schema).validate(data).to_a.map do |error|
|
51
51
|
format_error(error)
|
52
52
|
end
|
53
53
|
raise ResponseBodyInvalidError, errors.join(', ') if errors.any?
|
54
54
|
end
|
55
55
|
|
56
|
+
def load_json(string)
|
57
|
+
MultiJson.load(string)
|
58
|
+
rescue MultiJson::ParseError
|
59
|
+
string
|
60
|
+
end
|
61
|
+
|
56
62
|
def format_error(error)
|
57
63
|
err = ValidationFormat.error_details(error)
|
58
|
-
[err[:title],
|
64
|
+
[err[:title], error['data_pointer'], err[:detail]].compact.join(' ')
|
59
65
|
end
|
60
66
|
end
|
61
67
|
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
|
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.12.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-06-
|
11
|
+
date: 2020-06-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deep_merge
|