openapi_first 0.12.0.alpha1 → 0.12.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 476fbf7b7c6b6d5111d7dcb1fa4cd5197744c989315ea7b82b4e648956d7fee4
4
- data.tar.gz: 10e9f751d733284f21eccbad29cf12347ce9924eeba6ed4c4afd43450c952e2e
3
+ metadata.gz: 4d0848ff82fa49a93053ec107ca28c419c1be8b9122301a8819742685e8b7995
4
+ data.tar.gz: 11cf9a3c60b619cd55d7f3f552a2c367553aa69ed047ddd101eff1b460ee4c53
5
5
  SHA512:
6
- metadata.gz: b0e7387ededf44b1613c8de8663590086ecc063f940f603e52cae799db52600ac257767bbe5c861ad856270a1c4ac9af8d4eb65b8cdc0166fb84f56e05487f8f
7
- data.tar.gz: 636a22a59d02af532543e78f46ba7cb2b1acca3f329ff4a75410e3ccedd7f3c95b999bf99c54842d110f0d605827d935737fc58fa4b048c26df44b34f285d46c
6
+ metadata.gz: 5cabc3434dd9c4801b3f6ec030ec4ad5ef6905762a2643ef59a99f78057e6b108f90bf24696d29cab776dfa61a571be4973af7f62af07f5672f64996fb77086d
7
+ data.tar.gz: b7391c86d982be35d680e56f904261b8cfbd846a8206c2f339adc703248ad2476121fb8534a9562a20e081a2e5d5a8696231937affb23a72fe917a546a7e34c2
@@ -1,8 +1,12 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased (0.12.x)
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (0.12.0.alpha1)
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.2)
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.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- openapi_first (0.11.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.0)
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)
@@ -13,4 +13,9 @@ module Web
13
13
  end
14
14
 
15
15
  oas_path = File.absolute_path('./openapi.yaml', __dir__)
16
- App = OpenapiFirst.app(oas_path, namespace: Web)
16
+ pp OpenapiFirst.env == 'test'
17
+ App = OpenapiFirst.app(
18
+ oas_path,
19
+ namespace: Web,
20
+ raise_error: OpenapiFirst.env == 'test'
21
+ )
@@ -31,7 +31,7 @@ module OpenapiFirst
31
31
  Definition.new(parsed)
32
32
  end
33
33
 
34
- def self.app(spec, namespace:, raise_error: OpenapiFirst.env == 'test')
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
@@ -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
- end
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 = find_by_operation_id(operation_id)
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 find_by_operation_id(operation_id) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
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
- if name.include?('.')
29
- module_name, method_name = name.split('.')
30
- klass = find_const(@namespace, module_name)
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
- private
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
- return unless parent.const_defined?(name, false)
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 media type found: '#{content_type}' for '#{operation_name}'"
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 '#{operation_name}'"
48
+ message = "Response status code or default not found: #{status} for '#{name}'"
49
49
  raise OpenapiFirst::ResponseCodeNotFoundError, message
50
50
  end
51
51
 
52
- private
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
- @handlers = FindHandler.new(spec, namespace).all
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 = @handlers[operation.operation_id]
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? ? {} : MultiJson.load(full_body)
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], 'at', error['data_pointer'], err[:detail]].compact.join(' ')
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: 'is not valid' }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.12.0.alpha1'
4
+ VERSION = '0.12.0.alpha2'
5
5
  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.alpha1
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-05 00:00:00.000000000 Z
11
+ date: 2020-06-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deep_merge