openapi_first 0.11.0 → 0.12.0.alpha1

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