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 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