openapi_first 0.10.1 → 0.12.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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