openapi_first 0.11.0 → 0.12.0.alpha1

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: 3a7011597803fb49d100073027c9545634d497971b34ac49c2eeb0916258f424
4
- data.tar.gz: 1f552e13bd915d41d39e2fffcc7a026b5abd3ba5c19ed4f726e3538c68a893df
3
+ metadata.gz: 476fbf7b7c6b6d5111d7dcb1fa4cd5197744c989315ea7b82b4e648956d7fee4
4
+ data.tar.gz: 10e9f751d733284f21eccbad29cf12347ce9924eeba6ed4c4afd43450c952e2e
5
5
  SHA512:
6
- metadata.gz: 50053573288bde3ce62df411c706dc9fe64a4395b7110a539cf4e1b685b1953140cdbc8d9ad7e7d8b0eee4d9b4211243f7c3e85ef6b79a44720f9b56ae316c3f
7
- data.tar.gz: 017b082c94490f45cc0cb90935c0f5a463958740d858b56f2d6baf1b2e579f9e5820acda5cc5f53c0d9fe50cc261600a1f8c93c498f165997f026f68617c888f
6
+ metadata.gz: b0e7387ededf44b1613c8de8663590086ecc063f940f603e52cae799db52600ac257767bbe5c861ad856270a1c4ac9af8d4eb65b8cdc0166fb84f56e05487f8f
7
+ data.tar.gz: 636a22a59d02af532543e78f46ba7cb2b1acca3f329ff4a75410e3ccedd7f3c95b999bf99c54842d110f0d605827d935737fc58fa4b048c26df44b34f285d46c
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased (0.12.x)
4
+ - Rename `raise` option to `raise_error`
5
+ - Add `raise_error` option to RequestValidation middleware
6
+
3
7
  ## 0.11.0
4
8
  - Raise error if you forgot to add the Router middleware
5
9
  - Make OpenapiFirst.app raise an error in test env when request path is not specified
@@ -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.alpha1)
5
5
  deep_merge (>= 1.2.1)
6
6
  hanami-router (~> 2.0.alpha3)
7
7
  hanami-utils (~> 2.0.alpha1)
data/README.md CHANGED
@@ -31,7 +31,7 @@ Options and their defaults:
31
31
  |:---|---|---|---|
32
32
  |`spec:`| | The spec loaded via `OpenapiFirst.load` ||
33
33
  | `not_found:` |`nil`, `:continue`, `Proc`| Specifies what to do if the 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)
34
- | `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)
34
+ | `raise_error:` |`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)
35
35
 
36
36
  ## OpenapiFirst::RequestValidation
37
37
 
@@ -41,6 +41,13 @@ This middleware returns a 400 status code with a body that describes the error i
41
41
  use OpenapiFirst::RequestValidation
42
42
  ```
43
43
 
44
+
45
+ Options and their defaults:
46
+
47
+ | Name | Possible values | Description | Default
48
+ |:---|---|---|---|
49
+ | `raise_error:` |`false`, `true` | If set to true the middleware raises `OpenapiFirst::RequestInvalidError` instead of returning 4xx. | `false` (don't raise an exception)
50
+
44
51
  The error responses conform with [JSON:API](https://jsonapi.org).
45
52
 
46
53
  Here's an example response body for a missing query parameter "search":
@@ -117,7 +124,7 @@ There are two ways to set the response body:
117
124
  - Returning a value which will get converted to JSON
118
125
 
119
126
  ## OpenapiFirst::ResponseValidation
120
- This middleware is especially useful when testing. It raises an error if the response is not valid.
127
+ This middleware is especially useful when testing. It *always* raises an error if the response is not valid.
121
128
 
122
129
  ```ruby
123
130
  use OpenapiFirst::ResponseValidation if ENV['RACK_ENV'] == 'test'
@@ -19,6 +19,10 @@ module OpenapiFirst
19
19
  INBOX = 'openapi_first.inbox'
20
20
  HANDLER = 'openapi_first.handler'
21
21
 
22
+ def self.env
23
+ ENV['RACK_ENV'] || ENV['HANAMI_ENV'] || ENV['RAILS_ENV']
24
+ end
25
+
22
26
  def self.load(spec_path, only: nil)
23
27
  content = YAML.load_file(spec_path)
24
28
  raw = OasParser::Parser.new(spec_path, content).resolve
@@ -27,15 +31,14 @@ module OpenapiFirst
27
31
  Definition.new(parsed)
28
32
  end
29
33
 
30
- def self.app(spec, namespace:)
34
+ def self.app(spec, namespace:, raise_error: OpenapiFirst.env == 'test')
31
35
  spec = OpenapiFirst.load(spec) if spec.is_a?(String)
32
- test = ENV['RACK_ENV'] == 'test'
33
- App.new(nil, spec, namespace: namespace, router_raise: test)
36
+ App.new(nil, spec, namespace: namespace, raise_error: raise_error)
34
37
  end
35
38
 
36
- def self.middleware(spec, namespace:)
39
+ def self.middleware(spec, namespace:, raise_error: false)
37
40
  spec = OpenapiFirst.load(spec) if spec.is_a?(String)
38
- AppWithOptions.new(spec, namespace: namespace, router_raise: false)
41
+ AppWithOptions.new(spec, namespace: namespace, raise_error: raise_error)
39
42
  end
40
43
 
41
44
  class AppWithOptions
@@ -54,4 +57,36 @@ module OpenapiFirst
54
57
  class ResponseCodeNotFoundError < Error; end
55
58
  class ResponseMediaTypeNotFoundError < Error; end
56
59
  class ResponseBodyInvalidError < Error; end
60
+
61
+ class RequestInvalidError < Error
62
+ def initialize(serialized_errors)
63
+ message = error_message(serialized_errors)
64
+ super message
65
+ end
66
+
67
+ private
68
+
69
+ def error_message(errors)
70
+ errors.map do |error|
71
+ [human_source(error), human_error(error)].compact.join(' ')
72
+ end.join(', ')
73
+ end
74
+
75
+ def human_source(error)
76
+ return unless error[:source]
77
+
78
+ source_key = error[:source].keys.first
79
+ source = {
80
+ pointer: 'Request body invalid:',
81
+ parameter: 'Query parameter invalid:'
82
+ }.fetch(source_key, source_key)
83
+ name = error[:source].values.first
84
+ source += " #{name}" unless name.nil? || name.empty?
85
+ source
86
+ end
87
+
88
+ def human_error(error)
89
+ error[:title]
90
+ end
91
+ end
57
92
  end
@@ -5,11 +5,11 @@ require 'logger'
5
5
 
6
6
  module OpenapiFirst
7
7
  class App
8
- def initialize(parent_app, spec, namespace:, router_raise:)
8
+ def initialize(parent_app, spec, namespace:, raise_error:)
9
9
  @stack = Rack::Builder.app do
10
10
  freeze_app
11
- use OpenapiFirst::Router, spec: spec, raise: router_raise, parent_app: parent_app
12
- use OpenapiFirst::RequestValidation
11
+ use OpenapiFirst::Router, spec: spec, raise_error: raise_error, parent_app: parent_app
12
+ use OpenapiFirst::RequestValidation, raise_error: raise_error
13
13
  run OpenapiFirst::Responder.new(
14
14
  spec: spec,
15
15
  namespace: namespace
@@ -14,17 +14,5 @@ module OpenapiFirst
14
14
  def operations
15
15
  @spec.endpoints.map { |e| Operation.new(e) }
16
16
  end
17
-
18
- def find_operation!(request)
19
- @spec
20
- .path_by_path(request.path)
21
- .endpoint_by_method(request.request_method.downcase)
22
- end
23
-
24
- def find_operation(request)
25
- find_operation!(request)
26
- rescue OasParser::PathNotFound, OasParser::MethodNotFound
27
- nil
28
- end
29
17
  end
30
18
  end
@@ -11,8 +11,9 @@ module OpenapiFirst
11
11
  class RequestValidation # rubocop:disable Metrics/ClassLength
12
12
  prepend RouterRequired
13
13
 
14
- def initialize(app)
14
+ def initialize(app, raise_error: false)
15
15
  @app = app
16
+ @raise = raise_error
16
17
  end
17
18
 
18
19
  def call(env) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
@@ -85,6 +86,8 @@ module OpenapiFirst
85
86
  end
86
87
 
87
88
  def error_response(status, errors = [default_error(status)])
89
+ raise RequestInvalidError, errors if @raise
90
+
88
91
  Rack::Response.new(
89
92
  MultiJson.dump(errors: errors),
90
93
  status,
@@ -3,31 +3,33 @@
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
17
19
  Validation.new([e.message])
18
20
  end
19
21
 
20
22
  def validate_operation(request, response)
21
23
  errors = validation_errors(request, response)
22
24
  Validation.new(errors || [])
23
- rescue OasParser::ResponseCodeNotFound, OasParser::MethodNotFound => e
25
+ rescue OpenapiFirst::ResponseCodeNotFoundError, OpenapiFirst::NotFoundError => e
24
26
  Validation.new([e.message])
25
27
  end
26
28
 
27
29
  private
28
30
 
29
31
  def validation_errors(request, response)
30
- content = response_for(request, response)&.content
32
+ content = response_for(request, response)&.fetch('content', nil)
31
33
  return unless content
32
34
 
33
35
  content_type = content[response.content_type]
@@ -55,9 +57,10 @@ module OpenapiFirst
55
57
  end
56
58
 
57
59
  def response_for(request, response)
58
- @spec
59
- .find_operation!(request)
60
- &.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)
61
64
  end
62
65
  end
63
66
  end
@@ -9,24 +9,29 @@ module OpenapiFirst
9
9
  NOT_FOUND = Rack::Response.new('', 404).finish.freeze
10
10
  DEFAULT_NOT_FOUND_APP = ->(_env) { NOT_FOUND }
11
11
 
12
- def initialize(app, options) # rubocop:disable Metrics/MethodLength
12
+ def initialize(
13
+ app,
14
+ spec:,
15
+ raise_error: false,
16
+ parent_app: nil,
17
+ not_found: nil
18
+ )
13
19
  @app = app
14
- @parent_app = options.fetch(:parent_app, nil)
15
- @raise = options.fetch(:raise, false)
16
- @failure_app = find_failure_app(options[:not_found])
20
+ @parent_app = parent_app
21
+ @raise = raise_error
22
+ @failure_app = find_failure_app(not_found)
17
23
  if @failure_app.nil?
18
24
  raise ArgumentError,
19
25
  'not_found must be nil, :continue or must respond to call'
20
26
  end
21
- spec = options.fetch(:spec)
22
27
  @filepath = spec.filepath
23
28
  @router = build_router(spec.operations)
24
29
  end
25
30
 
26
31
  def call(env)
27
32
  env[OPERATION] = nil
28
- endpoint = find_endpoint(env)
29
- return endpoint.call(env) if endpoint
33
+ route = find_route(env)
34
+ return route.call(env) if route.routable?
30
35
 
31
36
  if @raise
32
37
  req = Rack::Request.new(env)
@@ -47,10 +52,10 @@ module OpenapiFirst
47
52
  option if option.respond_to?(:call)
48
53
  end
49
54
 
50
- def find_endpoint(env)
55
+ def find_route(env)
51
56
  original_path_info = env[Rack::PATH_INFO]
52
57
  env[Rack::PATH_INFO] = Rack::Request.new(env).path
53
- @router.recognize(env).endpoint
58
+ @router.recognize(env)
54
59
  ensure
55
60
  env[Rack::PATH_INFO] = original_path_info
56
61
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.11.0'
4
+ VERSION = '0.12.0.alpha1'
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.alpha3'
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.11.0
4
+ version: 0.12.0.alpha1
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-03 00:00:00.000000000 Z
11
+ date: 2020-06-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deep_merge
@@ -236,9 +236,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
236
236
  version: '0'
237
237
  required_rubygems_version: !ruby/object:Gem::Requirement
238
238
  requirements:
239
- - - ">="
239
+ - - ">"
240
240
  - !ruby/object:Gem::Version
241
- version: '0'
241
+ version: 1.3.1
242
242
  requirements: []
243
243
  rubygems_version: 3.1.2
244
244
  signing_key: