openapi_first 0.10.2 → 0.11.0.alpha

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: 49ebcf2af159defac87d97f5d9cc1b2c76a1f76ce09376f209bc38458c2eee07
4
- data.tar.gz: dbe17ecf51792614df624c91a704f9a319bf1bf2b1b1cded454a748f193c2e21
3
+ metadata.gz: c16372f591f02e7332d7d010c0a91e239e38592f41bf2eb5d88d7777058f66fd
4
+ data.tar.gz: e02ab1188d3cd8bc64ece82381316f4d7e8012c784e21d1ae2bb0b16bc24b141
5
5
  SHA512:
6
- metadata.gz: 9d478201cf1e856d745dff02e4977552b95c09500956be61c6d169cf179a1f15968bca460a63a9ce76fb97bba0c5d5713e94d5e802d8fe0446428fdfd6b3feb8
7
- data.tar.gz: dac17e6ec186b821a6fbdf09c47c20478f43bb21d30175b5d3e8150e0e99cf98110b2a7c01e143207176733485b52df1972277bc6790a7730c6a365dde32e3b1
6
+ metadata.gz: 8d8f44debbe60e787de68246b822669991ba6d1aac3721cbe68f04ee51cffb99ea02b280be0ba3e6e28d0bf11a26b565d2e23792fb08f451462ab36990026970
7
+ data.tar.gz: 2545816e97f18afde6947c61893b81a2606fa5e27262d037f3fd1475430acf7c1ffa2bf5db70b6d06316cd6524380ac47290b656ae457200e29e8384da860dc9
data/.rubocop.yml CHANGED
@@ -8,10 +8,16 @@ Metrics/BlockLength:
8
8
  Exclude:
9
9
  - 'spec/**/*.rb'
10
10
  - '*.gemspec'
11
+ Layout/EmptyLinesAroundAttributeAccessor:
12
+ Enabled: true
11
13
  Layout/SpaceAroundMethodCallOperator:
12
14
  Enabled: true
15
+ Lint/DeprecatedOpenSSLConstant:
16
+ Enabled: true
13
17
  Lint/RaiseException:
14
18
  Enabled: true
19
+ Style/SlicingWithRange:
20
+ Enabled: true
15
21
  Lint/StructNewOverride:
16
22
  Enabled: true
17
23
  Style/HashEachMethods:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+ - Add ResponseValidation middleware that validates the response body
5
+ - Add `raise` option to Router middleware to raise an error if request could not be found in the API description similar to committee's raise option.
6
+ - Move namespace option from Router to OperationResolver
7
+
3
8
  ## 0.10.2
4
9
  - Return 400 if request body has invalid JSON ([issue](https://github.com/ahx/openapi_first/issues/73)) thanks Thomas Frütel
5
10
 
data/Gemfile.lock CHANGED
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (0.10.2)
4
+ openapi_first (0.11.0.alpha)
5
5
  deep_merge (>= 1.2.1)
6
- hanami-router (~> 2.0.alpha2)
6
+ hanami-router (~> 2.0.alpha3)
7
7
  hanami-utils (~> 2.0.alpha1)
8
8
  json_schemer (~> 0.2)
9
9
  multi_json (~> 1.14)
@@ -13,7 +13,7 @@ PATH
13
13
  GEM
14
14
  remote: https://rubygems.org/
15
15
  specs:
16
- activesupport (6.0.3)
16
+ activesupport (6.0.3.1)
17
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
18
18
  i18n (>= 0.7, < 2)
19
19
  minitest (~> 5.1)
@@ -30,7 +30,7 @@ GEM
30
30
  ecma-re-validator (0.2.1)
31
31
  regexp_parser (~> 1.2)
32
32
  hana (1.3.6)
33
- hanami-router (2.0.0.alpha2)
33
+ hanami-router (2.0.0.alpha3)
34
34
  mustermann (~> 1.0)
35
35
  mustermann-contrib (~> 1.0)
36
36
  rack (~> 2.0)
@@ -41,7 +41,6 @@ GEM
41
41
  hash-deep-merge (0.1.1)
42
42
  i18n (1.8.2)
43
43
  concurrent-ruby (~> 1.0)
44
- jaro_winkler (1.5.4)
45
44
  json_schemer (0.2.11)
46
45
  ecma-re-validator (~> 0.2)
47
46
  hana (~> 1.3)
@@ -49,7 +48,7 @@ GEM
49
48
  uri_template (~> 0.7)
50
49
  method_source (1.0.0)
51
50
  mini_portile2 (2.4.0)
52
- minitest (5.14.0)
51
+ minitest (5.14.1)
53
52
  multi_json (1.14.1)
54
53
  mustermann (1.1.1)
55
54
  ruby2_keywords (~> 0.0.1)
@@ -72,7 +71,7 @@ GEM
72
71
  pry (0.13.1)
73
72
  coderay (~> 1.1)
74
73
  method_source (~> 1.0)
75
- public_suffix (4.0.4)
74
+ public_suffix (4.0.5)
76
75
  rack (2.2.2)
77
76
  rack-test (1.1.0)
78
77
  rack (>= 1.0, < 3)
@@ -86,21 +85,23 @@ GEM
86
85
  rspec-mocks (~> 3.9.0)
87
86
  rspec-core (3.9.2)
88
87
  rspec-support (~> 3.9.3)
89
- rspec-expectations (3.9.1)
88
+ rspec-expectations (3.9.2)
90
89
  diff-lcs (>= 1.2.0, < 2.0)
91
90
  rspec-support (~> 3.9.0)
92
91
  rspec-mocks (3.9.1)
93
92
  diff-lcs (>= 1.2.0, < 2.0)
94
93
  rspec-support (~> 3.9.0)
95
94
  rspec-support (3.9.3)
96
- rubocop (0.82.0)
97
- jaro_winkler (~> 1.5.1)
95
+ rubocop (0.84.0)
98
96
  parallel (~> 1.10)
99
97
  parser (>= 2.7.0.1)
100
98
  rainbow (>= 2.2.2, < 4.0)
101
99
  rexml
100
+ rubocop-ast (>= 0.0.3)
102
101
  ruby-progressbar (~> 1.7)
103
102
  unicode-display_width (>= 1.4.0, < 2.0)
103
+ rubocop-ast (0.0.3)
104
+ parser (>= 2.7.0.1)
104
105
  ruby-progressbar (1.10.1)
105
106
  ruby2_keywords (0.0.2)
106
107
  thread_safe (0.3.6)
data/README.md CHANGED
@@ -7,10 +7,19 @@ Start with writing an OpenAPI file that describes the API, which you are about t
7
7
  ## Rack middlewares
8
8
  OpenapiFirst consists of these Rack middlewares:
9
9
 
10
- - `OpenapiFirst::Router` finds the operation for the current request or returns 404 if no operation was found.
11
- - `OpenapiFirst::RequestValidation` validates the request against the found operation and returns 400 if the request is invalid.
10
+ - `OpenapiFirst::Router` Finds the operation for the current request or returns 404 if no operation was found. This can be customized.
11
+ - `OpenapiFirst::RequestValidation` Validates the request against the API description and returns 400 if the request is invalid.
12
12
  - `OpenapiFirst::OperationResolver` calls the [handler](#handlers) found for the operation.
13
13
 
14
+ ## OpenapiFirst::Router
15
+ Options and their defaults:
16
+
17
+ | Name | Possible values | Description | Default
18
+ |:---|---|---|---|
19
+ | `not_found:` |`nil`, `:continue`, `Proc`| Specifies what to do if path was not found in the API description. `nil` (default) returns a 404 response. `:continue` does nothing an calls the next app. `Proc` (or something that responds to `call`) to customize the response. | `nil` (return 404)
20
+ | `raise:` |`false`, `true` | If set to true the middleware raises `OpenapiFirst::NotFoundError` when a path or method was not found in the API description. This is useful during testing to spot an incomplete API description. | `false` (don't raise an exception)
21
+
22
+
14
23
  ## Usage within your Rack webframework
15
24
  If you just want to use the request validation part without any handlers you can use the rack middlewares standalone:
16
25
 
@@ -25,6 +34,7 @@ These variables will available in your rack env:
25
34
  - `env[OpenapiFirst::OPERATION]` - Holds an Operation object that responsed about `operation_id` and `path`. This is useful for introspection.
26
35
  - `env[OpenapiFirst::INBOX]`. Holds the (filtered) path and query parameters and the parsed request body.
27
36
 
37
+
28
38
  ## Standalone usage
29
39
  You can implement your API in conveniently with just OpenapiFirst.
30
40
 
@@ -204,7 +214,7 @@ Out of scope. Use [Prism](https://github.com/stoplightio/prism) or [fakeit](http
204
214
 
205
215
  ## Alternatives
206
216
 
207
- This gem is inspired by [committee](https://github.com/interagent/committee), which has much more features like response stubs or support for Hyper-Schema or OpenAPI 2.
217
+ This gem is inspired by [committee](https://github.com/interagent/committee) (Ruby) and [connexion](https://github.com/zalando/connexion) (Python).
208
218
 
209
219
  ## Development
210
220
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- openapi_first (0.10.2)
4
+ openapi_first (0.11.0.alpha)
5
5
  deep_merge (>= 1.2.1)
6
6
  hanami-router (~> 2.0.alpha2)
7
7
  hanami-utils (~> 2.0.alpha1)
@@ -13,7 +13,7 @@ PATH
13
13
  GEM
14
14
  remote: https://rubygems.org/
15
15
  specs:
16
- activesupport (6.0.3)
16
+ activesupport (6.0.3.1)
17
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
18
18
  i18n (>= 0.7, < 2)
19
19
  minitest (~> 5.1)
@@ -25,9 +25,9 @@ GEM
25
25
  benchmark-memory (0.1.2)
26
26
  memory_profiler (~> 0.9)
27
27
  builder (3.2.4)
28
- committee (3.3.0)
28
+ committee (4.0.0)
29
29
  json_schema (~> 0.14, >= 0.14.3)
30
- openapi_parser (>= 0.6.1)
30
+ openapi_parser (>= 0.11.1)
31
31
  rack (>= 1.5)
32
32
  concurrent-ruby (1.1.6)
33
33
  deep_merge (1.2.1)
@@ -55,7 +55,7 @@ GEM
55
55
  dry-logic (~> 1.0, >= 1.0.2)
56
56
  ecma-re-validator (0.2.1)
57
57
  regexp_parser (~> 1.2)
58
- grape (1.3.2)
58
+ grape (1.3.3)
59
59
  activesupport
60
60
  builder
61
61
  dry-types (>= 1.1)
@@ -63,7 +63,7 @@ GEM
63
63
  rack (>= 1.3.0)
64
64
  rack-accept
65
65
  hana (1.3.6)
66
- hanami-router (2.0.0.alpha2)
66
+ hanami-router (2.0.0.alpha3)
67
67
  mustermann (~> 1.0)
68
68
  mustermann-contrib (~> 1.0)
69
69
  rack (~> 2.0)
@@ -82,7 +82,7 @@ GEM
82
82
  uri_template (~> 0.7)
83
83
  memory_profiler (0.9.14)
84
84
  mini_portile2 (2.4.0)
85
- minitest (5.14.0)
85
+ minitest (5.14.1)
86
86
  multi_json (1.14.1)
87
87
  mustermann (1.1.1)
88
88
  ruby2_keywords (~> 0.0.1)
@@ -101,8 +101,8 @@ GEM
101
101
  hash-deep-merge
102
102
  mustermann-contrib (~> 1.1.1)
103
103
  nokogiri
104
- openapi_parser (0.10.0)
105
- public_suffix (4.0.4)
104
+ openapi_parser (0.11.2)
105
+ public_suffix (4.0.5)
106
106
  rack (2.2.2)
107
107
  rack-accept (0.4.5)
108
108
  rack (>= 0.4)
data/lib/openapi_first.rb CHANGED
@@ -8,6 +8,7 @@ require 'openapi_first/inbox'
8
8
  require 'openapi_first/router'
9
9
  require 'openapi_first/request_validation'
10
10
  require 'openapi_first/response_validator'
11
+ require 'openapi_first/response_validation'
11
12
  require 'openapi_first/operation_resolver'
12
13
  require 'openapi_first/app'
13
14
 
@@ -48,5 +49,8 @@ module OpenapiFirst
48
49
  end
49
50
 
50
51
  class Error < StandardError; end
52
+ class NotFoundError < Error; end
51
53
  class ResponseCodeNotFoundError < Error; end
54
+ class ResponseMediaTypeNotFoundError < Error; end
55
+ class ResponseBodyInvalidError < Error; end
52
56
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rack'
4
+ require 'logger'
4
5
 
5
6
  module OpenapiFirst
6
7
  class App
@@ -11,12 +12,12 @@ module OpenapiFirst
11
12
  )
12
13
  @stack = Rack::Builder.app do
13
14
  freeze_app
14
- use OpenapiFirst::Router,
15
- spec: spec,
16
- namespace: namespace,
17
- parent_app: parent_app
15
+ use OpenapiFirst::Router, spec: spec, parent_app: parent_app
18
16
  use OpenapiFirst::RequestValidation
19
- run OpenapiFirst::OperationResolver.new
17
+ run OpenapiFirst::OperationResolver.new(
18
+ spec: spec,
19
+ namespace: namespace
20
+ )
20
21
  end
21
22
  end
22
23
 
@@ -4,7 +4,10 @@ require_relative 'operation'
4
4
 
5
5
  module OpenapiFirst
6
6
  class Definition
7
+ attr_reader :filepath
8
+
7
9
  def initialize(parsed)
10
+ @filepath = parsed.path
8
11
  @spec = parsed
9
12
  end
10
13
 
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils'
4
+
5
+ module OpenapiFirst
6
+ class FindHandler
7
+ def initialize(spec, namespace)
8
+ @spec = spec
9
+ @namespace = namespace
10
+ end
11
+
12
+ def all
13
+ @spec.operations.each_with_object({}) do |operation, hash|
14
+ operation_id = operation.operation_id
15
+ handler = find_by_operation_id(operation_id)
16
+ if handler.nil?
17
+ 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
+ next
19
+ end
20
+ hash[operation_id] = handler
21
+ end
22
+ end
23
+
24
+ def find_by_operation_id(operation_id) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
25
+ name = operation_id.match(/:*(.*)/)&.to_a&.at(1)
26
+ return if name.nil?
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) }
40
+ end
41
+ method_name = Utils.underscore(name)
42
+ return unless @namespace.respond_to?(method_name)
43
+
44
+ @namespace.method(method_name)
45
+ end
46
+
47
+ private
48
+
49
+ def find_const(parent, name)
50
+ name = Utils.classify(name)
51
+ return unless parent.const_defined?(name, false)
52
+
53
+ parent.const_get(name, false)
54
+ end
55
+ end
56
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'forwardable'
4
4
  require_relative 'utils'
5
+ require_relative 'response_object'
5
6
 
6
7
  module OpenapiFirst
7
8
  class Operation
@@ -25,18 +26,35 @@ module OpenapiFirst
25
26
  end
26
27
 
27
28
  def content_type_for(status)
28
- content = @operation
29
- .response_by_code(status.to_s, use_default: true)
30
- .content
29
+ content = response_for(status)['content']
31
30
  content.keys[0] if content
31
+ end
32
+
33
+ def response_schema_for(status, content_type)
34
+ content = response_for(status)['content']
35
+ return if content.nil? || content.empty?
36
+
37
+ media_type = content[content_type]
38
+ unless media_type
39
+ message = "Response media type found: '#{content_type}' for '#{operation_name}'"
40
+ raise ResponseMediaTypeNotFoundError, message
41
+ end
42
+ media_type['schema']
43
+ end
44
+
45
+ def response_for(status)
46
+ @operation.response_by_code(status.to_s, use_default: true).raw
32
47
  rescue OasParser::ResponseCodeNotFound
33
- operation_name = "#{method.upcase} #{path}"
34
- message = "Response status code or default not found: #{status} for '#{operation_name}'" # rubocop:disable Layout/LineLength
48
+ message = "Response status code or default not found: #{status} for '#{operation_name}'"
35
49
  raise OpenapiFirst::ResponseCodeNotFoundError, message
36
50
  end
37
51
 
38
52
  private
39
53
 
54
+ def operation_name
55
+ "#{method.upcase} #{path}"
56
+ end
57
+
40
58
  def build_parameters_json_schema
41
59
  return unless @operation.parameters&.any?
42
60
 
@@ -2,15 +2,20 @@
2
2
 
3
3
  require 'rack'
4
4
  require_relative 'inbox'
5
+ require_relative 'find_handler'
5
6
 
6
7
  module OpenapiFirst
7
8
  class OperationResolver
9
+ def initialize(spec:, namespace:)
10
+ @handlers = FindHandler.new(spec, namespace).all
11
+ @namespace = namespace
12
+ end
13
+
8
14
  def call(env)
9
15
  operation = env[OpenapiFirst::OPERATION]
10
16
  res = Rack::Response.new
11
- inbox = env[INBOX]
12
- handler = env[HANDLER]
13
- result = handler.call(inbox, res)
17
+ handler = @handlers[operation.operation_id]
18
+ result = handler.call(env[INBOX], res)
14
19
  res.write serialize(result) if result && res.body.empty?
15
20
  res[Rack::CONTENT_TYPE] ||= operation.content_type_for(res.status)
16
21
  res.finish
@@ -46,9 +46,7 @@ module OpenapiFirst
46
46
 
47
47
  parsed_request_body = parse_request_body!(body)
48
48
  errors = validate_json_schema(schema, parsed_request_body)
49
- if errors.any?
50
- halt(error_response(400, serialize_request_body_errors(errors)))
51
- end
49
+ halt(error_response(400, serialize_request_body_errors(errors))) if errors.any?
52
50
  env[INBOX].merge! env[REQUEST_BODY] = parsed_request_body
53
51
  end
54
52
 
@@ -113,9 +111,7 @@ module OpenapiFirst
113
111
 
114
112
  params = filtered_params(json_schema, params)
115
113
  errors = JSONSchemer.schema(json_schema).validate(params)
116
- if errors.any?
117
- halt error_response(400, serialize_query_parameter_errors(errors))
118
- end
114
+ halt error_response(400, serialize_query_parameter_errors(errors)) if errors.any?
119
115
  env[PARAMETERS] = params
120
116
  env[INBOX].merge! params
121
117
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require_relative 'utils'
5
+
6
+ module OpenapiFirst
7
+ # Represents an OpenAPI Response Object
8
+ class ResponseObject
9
+ extend Forwardable
10
+ def_delegators :@parsed,
11
+ :content
12
+
13
+ def_delegators :@raw,
14
+ :[]
15
+
16
+ def initialize(parsed)
17
+ @parsed = parsed
18
+ @raw = parsed.raw
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json_schemer'
4
+ require 'multi_json'
5
+ require_relative 'validation'
6
+
7
+ module OpenapiFirst
8
+ class ResponseValidation
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ operation = env[OPERATION]
15
+ return @app.call(env) unless operation
16
+
17
+ status, headers, body = @app.call(env)
18
+ content_type = headers[Rack::CONTENT_TYPE]
19
+ response_schema = operation.response_schema_for(status, content_type)
20
+ validate_response_body(response_schema, body) if response_schema
21
+
22
+ [status, headers, body]
23
+ end
24
+
25
+ private
26
+
27
+ def halt(status, body = '')
28
+ throw :halt, [status, {}, body]
29
+ end
30
+
31
+ def error(message)
32
+ { title: message }
33
+ end
34
+
35
+ def error_response(status, errors)
36
+ Rack::Response.new(
37
+ MultiJson.dump(errors: errors),
38
+ status,
39
+ Rack::CONTENT_TYPE => 'application/vnd.api+json'
40
+ ).finish
41
+ end
42
+
43
+ def validate_response_body(schema, response)
44
+ full_body = +''
45
+ response.each { |chunk| full_body << chunk }
46
+ data = full_body.empty? ? {} : MultiJson.load(full_body)
47
+ errors = JSONSchemer.schema(schema).validate(data).to_a.map do |error|
48
+ format_error(error)
49
+ end
50
+ raise ResponseBodyInvalidError, errors.join(', ') if errors.any?
51
+ end
52
+
53
+ def format_error(error)
54
+ err = ValidationFormat.error_details(error)
55
+ [err[:title], 'at', error['data_pointer'], err[:detail]].compact.join(' ')
56
+ end
57
+ end
58
+ end
59
+
60
+ # frozen_string_literal: true
61
+
62
+ require 'json_schemer'
63
+ require 'multi_json'
64
+ require_relative 'validation'
65
+
66
+ module OpenapiFirst
67
+ class ResponseValidator
68
+ def initialize(spec)
69
+ @spec = spec
70
+ end
71
+
72
+ def validate(request, response)
73
+ errors = validation_errors(request, response)
74
+ Validation.new(errors || [])
75
+ rescue OasParser::ResponseCodeNotFound, OasParser::MethodNotFound => e
76
+ Validation.new([e.message])
77
+ end
78
+
79
+ private
80
+
81
+ def validation_errors(request, response)
82
+ content = response_for(request, response)&.content
83
+ return unless content
84
+
85
+ content_type = content[response.content_type]
86
+ return ["Content type not found: '#{response.content_type}'"] unless content_type
87
+
88
+ response_schema = content_type['schema']
89
+ return unless response_schema
90
+
91
+ response_data = MultiJson.load(response.body)
92
+ validate_json_schema(response_schema, response_data)
93
+ end
94
+
95
+ def validate_json_schema(schema, data)
96
+ JSONSchemer.schema(schema).validate(data).to_a.map do |error|
97
+ format_error(error)
98
+ end
99
+ end
100
+
101
+ def format_error(error)
102
+ ValidationFormat.error_details(error)
103
+ .merge!(
104
+ data_pointer: error['data_pointer'],
105
+ schema_pointer: error['schema_pointer']
106
+ ).tap do |formatted|
107
+ end
108
+ end
109
+
110
+ def response_for(request, response)
111
+ @spec
112
+ .find_operation!(request)
113
+ &.response_by_code(response.status.to_s, use_default: true)
114
+ end
115
+ end
116
+ end
@@ -17,6 +17,13 @@ module OpenapiFirst
17
17
  Validation.new([e.message])
18
18
  end
19
19
 
20
+ def validate_operation(request, response)
21
+ errors = validation_errors(request, response)
22
+ Validation.new(errors || [])
23
+ rescue OasParser::ResponseCodeNotFound, OasParser::MethodNotFound => e
24
+ Validation.new([e.message])
25
+ end
26
+
20
27
  private
21
28
 
22
29
  def validation_errors(request, response)
@@ -24,9 +31,7 @@ module OpenapiFirst
24
31
  return unless content
25
32
 
26
33
  content_type = content[response.content_type]
27
- unless content_type
28
- return ["Content type not found: '#{response.content_type}'"]
29
- end
34
+ return ["Content type not found: '#{response.content_type}'"] unless content_type
30
35
 
31
36
  response_schema = content_type['schema']
32
37
  return unless response_schema
@@ -46,8 +51,7 @@ module OpenapiFirst
46
51
  .merge!(
47
52
  data_pointer: error['data_pointer'],
48
53
  schema_pointer: error['schema_pointer']
49
- ).tap do |formatted|
50
- end
54
+ )
51
55
  end
52
56
 
53
57
  def response_for(request, response)
@@ -7,76 +7,59 @@ 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(app, options)
12
+ def initialize(app, options) # rubocop:disable Metrics/MethodLength
12
13
  @app = app
13
- @namespace = options.fetch(:namespace, nil)
14
14
  @parent_app = options.fetch(:parent_app, nil)
15
- @router = build_router(options.fetch(:spec).operations)
15
+ @raise = options.fetch(:raise, false)
16
+ @failure_app = find_failure_app(options[:not_found])
17
+ if @failure_app.nil?
18
+ raise ArgumentError,
19
+ 'not_found must be nil, :continue or must respond to call'
20
+ end
21
+ spec = options.fetch(:spec)
22
+ @filepath = spec.filepath
23
+ @router = build_router(spec.operations)
16
24
  end
17
25
 
18
26
  def call(env)
19
27
  endpoint = find_endpoint(env)
20
28
  return endpoint.call(env) if endpoint
29
+
30
+ if @raise
31
+ req = Rack::Request.new(env)
32
+ msg = "Could not find definition for #{req.request_method} '#{req.path}' in API description #{@filepath}"
33
+ raise NotFoundError, msg
34
+ end
21
35
  return @parent_app.call(env) if @parent_app
22
36
 
23
- NOT_FOUND
37
+ @failure_app.call(env)
24
38
  end
25
39
 
26
- def find_handler(operation_id) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
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
40
+ private
42
41
 
43
- return ->(params, res) { klass.new(params.env).call(params, res) }
44
- end
45
- method_name = Utils.underscore(name)
46
- return unless @namespace.respond_to?(method_name)
42
+ def find_failure_app(option)
43
+ return DEFAULT_NOT_FOUND_APP if option.nil?
44
+ return @app if option == :continue
47
45
 
48
- @namespace.method(method_name)
46
+ option if option.respond_to?(:call)
49
47
  end
50
48
 
51
- private
52
-
53
49
  def find_endpoint(env)
54
50
  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
51
  env[Rack::PATH_INFO] = Rack::Request.new(env).path
57
52
  @router.recognize(env).endpoint
58
53
  ensure
59
54
  env[Rack::PATH_INFO] = original_path_info
60
55
  end
61
56
 
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
57
  def build_router(operations) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
70
58
  router = Hanami::Router.new {}
71
59
  operations.each do |operation|
72
60
  normalized_path = operation.path.gsub('{', ':').gsub('}', '')
73
61
  if operation.operation_id.nil?
74
- warn "operationId is missing in '#{operation.method} #{operation.path}'. I am ignoring this operation." # rubocop:disable Layout/LineLength
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
62
+ warn "operationId is missing in '#{operation.method} #{operation.path}'. I am ignoring this operation."
80
63
  next
81
64
  end
82
65
  router.public_send(
@@ -85,7 +68,6 @@ module OpenapiFirst
85
68
  to: lambda do |env|
86
69
  env[OPERATION] = operation
87
70
  env[PARAMETERS] = Utils.deep_stringify(env['router.params'])
88
- env[HANDLER] = handler
89
71
  @app.call(env)
90
72
  end
91
73
  )
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.10.2'
4
+ VERSION = '0.11.0.alpha'
5
5
  end
@@ -33,7 +33,7 @@ Gem::Specification.new do |spec|
33
33
  spec.require_paths = ['lib']
34
34
 
35
35
  spec.add_dependency 'deep_merge', '>= 1.2.1'
36
- spec.add_dependency 'hanami-router', '~> 2.0.alpha2'
36
+ spec.add_dependency 'hanami-router', '~> 2.0.alpha3'
37
37
  spec.add_dependency 'hanami-utils', '~> 2.0.alpha1'
38
38
  spec.add_dependency 'json_schemer', '~> 0.2'
39
39
  spec.add_dependency 'multi_json', '~> 1.14'
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.10.2
4
+ version: 0.11.0.alpha
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-05-08 00:00:00.000000000 Z
11
+ date: 2020-05-24 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.alpha2
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.alpha2
40
+ version: 2.0.alpha3
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: hanami-utils
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -203,10 +203,13 @@ 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
209
  - lib/openapi_first/operation_resolver.rb
209
210
  - lib/openapi_first/request_validation.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
212
215
  - lib/openapi_first/utils.rb
@@ -232,9 +235,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
232
235
  version: '0'
233
236
  required_rubygems_version: !ruby/object:Gem::Requirement
234
237
  requirements:
235
- - - ">="
238
+ - - ">"
236
239
  - !ruby/object:Gem::Version
237
- version: '0'
240
+ version: 1.3.1
238
241
  requirements: []
239
242
  rubygems_version: 3.1.2
240
243
  signing_key: