openapi_first 0.10.2 → 0.11.0.alpha

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