openapi_first 0.10.1 → 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.
@@ -3,30 +3,37 @@
3
3
  require 'json_schemer'
4
4
  require 'multi_json'
5
5
  require_relative 'validation'
6
+ require_relative 'router'
6
7
 
7
8
  module OpenapiFirst
8
9
  class ResponseValidator
9
10
  def initialize(spec)
10
11
  @spec = spec
12
+ @router = Router.new(->(_env) {}, spec: spec, raise_error: true)
11
13
  end
12
14
 
13
15
  def validate(request, response)
14
16
  errors = validation_errors(request, response)
15
17
  Validation.new(errors || [])
16
- rescue OasParser::ResponseCodeNotFound, OasParser::MethodNotFound => e
18
+ rescue OpenapiFirst::ResponseCodeNotFoundError, OpenapiFirst::NotFoundError => e
19
+ Validation.new([e.message])
20
+ end
21
+
22
+ def validate_operation(request, response)
23
+ errors = validation_errors(request, response)
24
+ Validation.new(errors || [])
25
+ rescue OpenapiFirst::ResponseCodeNotFoundError, OpenapiFirst::NotFoundError => e
17
26
  Validation.new([e.message])
18
27
  end
19
28
 
20
29
  private
21
30
 
22
31
  def validation_errors(request, response)
23
- content = response_for(request, response)&.content
32
+ content = response_for(request, response)&.fetch('content', nil)
24
33
  return unless content
25
34
 
26
35
  content_type = content[response.content_type]
27
- unless content_type
28
- return ["Content type not found: '#{response.content_type}'"]
29
- end
36
+ return ["Content type not found: '#{response.content_type}'"] unless content_type
30
37
 
31
38
  response_schema = content_type['schema']
32
39
  return unless response_schema
@@ -46,14 +53,14 @@ module OpenapiFirst
46
53
  .merge!(
47
54
  data_pointer: error['data_pointer'],
48
55
  schema_pointer: error['schema_pointer']
49
- ).tap do |formatted|
50
- end
56
+ )
51
57
  end
52
58
 
53
59
  def response_for(request, response)
54
- @spec
55
- .find_operation!(request)
56
- &.response_by_code(response.status.to_s, use_default: true)
60
+ env = request.env.dup
61
+ @router.call(env)
62
+ operation = env[OPERATION]
63
+ operation&.response_for(response.status)
57
64
  end
58
65
  end
59
66
  end
@@ -7,76 +7,65 @@ 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(
13
+ app,
14
+ spec:,
15
+ raise_error: false,
16
+ parent_app: nil,
17
+ not_found: nil
18
+ )
12
19
  @app = app
13
- @namespace = options.fetch(:namespace, nil)
14
- @parent_app = options.fetch(:parent_app, nil)
15
- @router = build_router(options.fetch(:spec).operations)
20
+ @parent_app = parent_app
21
+ @raise = raise_error
22
+ @failure_app = find_failure_app(not_found)
23
+ if @failure_app.nil?
24
+ raise ArgumentError,
25
+ 'not_found must be nil, :continue or must respond to call'
26
+ end
27
+ @filepath = spec.filepath
28
+ @router = build_router(spec.operations)
16
29
  end
17
30
 
18
31
  def call(env)
19
- endpoint = find_endpoint(env)
20
- return endpoint.call(env) if endpoint
32
+ env[OPERATION] = nil
33
+ route = find_route(env)
34
+ return route.call(env) if route.routable?
35
+
36
+ if @raise
37
+ req = Rack::Request.new(env)
38
+ msg = "Could not find definition for #{req.request_method} '#{req.path}' in API description #{@filepath}"
39
+ raise NotFoundError, msg
40
+ end
21
41
  return @parent_app.call(env) if @parent_app
22
42
 
23
- NOT_FOUND
43
+ @failure_app.call(env)
24
44
  end
25
45
 
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
46
+ private
42
47
 
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)
48
+ def find_failure_app(option)
49
+ return DEFAULT_NOT_FOUND_APP if option.nil?
50
+ return @app if option == :continue
47
51
 
48
- @namespace.method(method_name)
52
+ option if option.respond_to?(:call)
49
53
  end
50
54
 
51
- private
52
-
53
- def find_endpoint(env)
55
+ def find_route(env)
54
56
  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
57
  env[Rack::PATH_INFO] = Rack::Request.new(env).path
57
- @router.recognize(env).endpoint
58
+ @router.recognize(env)
58
59
  ensure
59
60
  env[Rack::PATH_INFO] = original_path_info
60
61
  end
61
62
 
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
63
  def build_router(operations) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
70
64
  router = Hanami::Router.new {}
71
65
  operations.each do |operation|
72
66
  normalized_path = operation.path.gsub('{', ':').gsub('}', '')
73
67
  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
68
+ warn "operationId is missing in '#{operation.method} #{operation.path}'. I am ignoring this operation."
80
69
  next
81
70
  end
82
71
  router.public_send(
@@ -85,7 +74,6 @@ module OpenapiFirst
85
74
  to: lambda do |env|
86
75
  env[OPERATION] = operation
87
76
  env[PARAMETERS] = Utils.deep_stringify(env['router.params'])
88
- env[HANDLER] = handler
89
77
  @app.call(env)
90
78
  end
91
79
  )
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ module RouterRequired
5
+ def call(env)
6
+ unless env.key?(OPERATION)
7
+ raise 'OpenapiFirst::Router missing in middleware stack. Did you forget adding OpenapiFirst::Router?'
8
+ end
9
+
10
+ super
11
+ end
12
+ end
13
+ 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.10.1'
4
+ VERSION = '0.12.0.alpha2'
5
5
  end
@@ -32,13 +32,13 @@ Gem::Specification.new do |spec|
32
32
  spec.bindir = 'exe'
33
33
  spec.require_paths = ['lib']
34
34
 
35
- spec.add_dependency 'deep_merge', '>= 1.2.1'
36
- spec.add_dependency 'hanami-router', '~> 2.0.alpha2'
37
- spec.add_dependency 'hanami-utils', '~> 2.0.alpha1'
38
- spec.add_dependency 'json_schemer', '~> 0.2'
39
- spec.add_dependency 'multi_json', '~> 1.14'
40
- spec.add_dependency 'oas_parser', '~> 0.25.1'
41
- spec.add_dependency 'rack', '~> 2.2'
35
+ spec.add_runtime_dependency 'deep_merge', '>= 1.2.1'
36
+ spec.add_runtime_dependency 'hanami-router', '~> 2.0.alpha3'
37
+ spec.add_runtime_dependency 'hanami-utils', '~> 2.0.alpha1'
38
+ spec.add_runtime_dependency 'json_schemer', '~> 0.2'
39
+ spec.add_runtime_dependency 'multi_json', '~> 1.14'
40
+ spec.add_runtime_dependency 'oas_parser', '~> 0.25.1'
41
+ spec.add_runtime_dependency 'rack', '~> 2.2'
42
42
 
43
43
  spec.add_development_dependency 'bundler', '~> 2'
44
44
  spec.add_development_dependency 'rack-test', '~> 1'
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.1
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-05-08 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
@@ -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,12 +203,16 @@ 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
- - lib/openapi_first/operation_resolver.rb
209
209
  - lib/openapi_first/request_validation.rb
210
+ - lib/openapi_first/responder.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
215
+ - lib/openapi_first/router_required.rb
212
216
  - lib/openapi_first/utils.rb
213
217
  - lib/openapi_first/validation.rb
214
218
  - lib/openapi_first/validation_format.rb
@@ -232,9 +236,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
232
236
  version: '0'
233
237
  required_rubygems_version: !ruby/object:Gem::Requirement
234
238
  requirements:
235
- - - ">="
239
+ - - ">"
236
240
  - !ruby/object:Gem::Version
237
- version: '0'
241
+ version: 1.3.1
238
242
  requirements: []
239
243
  rubygems_version: 3.1.2
240
244
  signing_key:
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rack'
4
- require_relative 'inbox'
5
-
6
- module OpenapiFirst
7
- class OperationResolver
8
- def call(env)
9
- operation = env[OpenapiFirst::OPERATION]
10
- res = Rack::Response.new
11
- inbox = env[INBOX]
12
- handler = env[HANDLER]
13
- result = handler.call(inbox, res)
14
- res.write serialize(result) if result && res.body.empty?
15
- res[Rack::CONTENT_TYPE] ||= operation.content_type_for(res.status)
16
- res.finish
17
- end
18
-
19
- private
20
-
21
- def serialize(result)
22
- return result if result.is_a?(String)
23
-
24
- MultiJson.dump(result)
25
- end
26
- end
27
- end