openapi_first 0.12.0.alpha2 → 0.12.4

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: 4d0848ff82fa49a93053ec107ca28c419c1be8b9122301a8819742685e8b7995
4
- data.tar.gz: 11cf9a3c60b619cd55d7f3f552a2c367553aa69ed047ddd101eff1b460ee4c53
3
+ metadata.gz: c55b490840191990eac078738a26002b09f794b0f555ae1144faa679fadd7697
4
+ data.tar.gz: 855132c7c8893b9c02670926c820273aa4b7a407a6e7d02dd99ad2f600dd714f
5
5
  SHA512:
6
- metadata.gz: 5cabc3434dd9c4801b3f6ec030ec4ad5ef6905762a2643ef59a99f78057e6b108f90bf24696d29cab776dfa61a571be4973af7f62af07f5672f64996fb77086d
7
- data.tar.gz: b7391c86d982be35d680e56f904261b8cfbd846a8206c2f339adc703248ad2476121fb8534a9562a20e081a2e5d5a8696231937affb23a72fe917a546a7e34c2
6
+ metadata.gz: a1acd2cfa2197bcf9dd760cbf63e146d6ffcc5b9a4fef1b291a0d781c095a06976d02b40fae2b6393307b6f958f34b489514e7dcb22bb89df186c2c31c76f1fe
7
+ data.tar.gz: 8832ddbbc1101985806d0752f74ab2ca14cc7322def44cc0d3537dec3d04592dfce8b2c8d646d21b100e427176dc7a176c3472fca28fc85ed4ae881757a471d1
@@ -14,6 +14,8 @@ Layout/SpaceAroundMethodCallOperator:
14
14
  Enabled: true
15
15
  Lint/DeprecatedOpenSSLConstant:
16
16
  Enabled: true
17
+ Lint/DuplicateElsifCondition:
18
+ Enabled: true
17
19
  Lint/RaiseException:
18
20
  Enabled: true
19
21
  Lint/MixedRegexpCaptureTypes:
@@ -28,7 +30,25 @@ Lint/StructNewOverride:
28
30
  Enabled: true
29
31
  Style/HashEachMethods:
30
32
  Enabled: false
33
+ Style/AccessorGrouping:
34
+ Enabled: true
35
+ Style/ArrayCoercion:
36
+ Enabled: true
37
+ Style/BisectedAttrAccessor:
38
+ Enabled: true
39
+ Style/CaseLikeIf:
40
+ Enabled: true
41
+ Style/HashAsLastArrayItem:
42
+ Enabled: true
43
+ Style/HashLikeCase:
44
+ Enabled: true
31
45
  Style/HashTransformKeys:
32
46
  Enabled: true
33
47
  Style/HashTransformValues:
34
48
  Enabled: true
49
+ Style/RedundantAssignment:
50
+ Enabled: true
51
+ Style/RedundantFetchBlock:
52
+ Enabled: true
53
+ Style/RedundantFileExtensionInRequire:
54
+ Enabled: true
@@ -1,6 +1,23 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.12.4
4
+ - content-type is found while ignoring additional content-type parameters (`application/json` is found when request/response content-type is `application/json; charset=UTF8`)
5
+ - Support wildcard mime-types when finding the content-type
6
+
7
+ ## 0.12.3
8
+ - Add `response_validation:`, `router_raise_error` options to standalone mode.
9
+
10
+ ## 0.12.2
11
+ - Allow response to have no media type object specified
12
+
13
+ ## 0.12.1
14
+ - Fix response when handler returns 404 or 405
15
+ - Don't validate the response content if status is 205 (no content)
16
+
17
+ ## 0.12.0
18
+ - Change `ResponseValidator` to raise an exception if it found a problem
19
+ - Params have symbolized keys now
20
+ - Remove `not_found` option from Router. Return 405 if HTTP verb is not allowed (via Hanami::Router)
4
21
  - Add `raise_error` option to OpenapiFirst.app (false by default)
5
22
  - Add ResponseValidation to OpenapiFirst.app if raise_error option is true
6
23
  - Rename `raise` option to `raise_error`
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (0.12.0.alpha2)
4
+ openapi_first (0.12.4)
5
5
  deep_merge (>= 1.2.1)
6
6
  hanami-router (~> 2.0.alpha3)
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.1)
16
+ activesupport (6.0.3.2)
17
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
18
18
  i18n (>= 0.7, < 2)
19
19
  minitest (~> 5.1)
@@ -21,12 +21,12 @@ GEM
21
21
  zeitwerk (~> 2.2, >= 2.2.2)
22
22
  addressable (2.7.0)
23
23
  public_suffix (>= 2.0.2, < 5.0)
24
- ast (2.4.0)
24
+ ast (2.4.1)
25
25
  builder (3.2.4)
26
26
  coderay (1.1.3)
27
27
  concurrent-ruby (1.1.6)
28
28
  deep_merge (1.2.1)
29
- diff-lcs (1.3)
29
+ diff-lcs (1.4.4)
30
30
  ecma-re-validator (0.2.1)
31
31
  regexp_parser (~> 1.2)
32
32
  hana (1.3.6)
@@ -39,9 +39,9 @@ GEM
39
39
  transproc (~> 1.0)
40
40
  hansi (0.2.0)
41
41
  hash-deep-merge (0.1.1)
42
- i18n (1.8.3)
42
+ i18n (1.8.4)
43
43
  concurrent-ruby (~> 1.0)
44
- json_schemer (0.2.11)
44
+ json_schemer (0.2.12)
45
45
  ecma-re-validator (~> 0.2)
46
46
  hana (~> 1.3)
47
47
  regexp_parser (~> 1.5)
@@ -49,13 +49,13 @@ GEM
49
49
  method_source (1.0.0)
50
50
  mini_portile2 (2.4.0)
51
51
  minitest (5.14.1)
52
- multi_json (1.14.1)
52
+ multi_json (1.15.0)
53
53
  mustermann (1.1.1)
54
54
  ruby2_keywords (~> 0.0.1)
55
55
  mustermann-contrib (1.1.1)
56
56
  hansi (~> 0.2.0)
57
57
  mustermann (= 1.1.1)
58
- nokogiri (1.10.9)
58
+ nokogiri (1.10.10)
59
59
  mini_portile2 (~> 2.4.0)
60
60
  oas_parser (0.25.1)
61
61
  activesupport (>= 4.0.0)
@@ -65,19 +65,19 @@ GEM
65
65
  hash-deep-merge
66
66
  mustermann-contrib (~> 1.1.1)
67
67
  nokogiri
68
- parallel (1.19.1)
69
- parser (2.7.1.3)
70
- ast (~> 2.4.0)
68
+ parallel (1.19.2)
69
+ parser (2.7.1.4)
70
+ ast (~> 2.4.1)
71
71
  pry (0.13.1)
72
72
  coderay (~> 1.1)
73
73
  method_source (~> 1.0)
74
74
  public_suffix (4.0.5)
75
- rack (2.2.2)
75
+ rack (2.2.3)
76
76
  rack-test (1.1.0)
77
77
  rack (>= 1.0, < 3)
78
78
  rainbow (3.0.0)
79
79
  rake (13.0.1)
80
- regexp_parser (1.7.0)
80
+ regexp_parser (1.7.1)
81
81
  rexml (3.2.4)
82
82
  rspec (3.9.0)
83
83
  rspec-core (~> 3.9.0)
@@ -92,16 +92,16 @@ GEM
92
92
  diff-lcs (>= 1.2.0, < 2.0)
93
93
  rspec-support (~> 3.9.0)
94
94
  rspec-support (3.9.3)
95
- rubocop (0.85.0)
95
+ rubocop (0.88.0)
96
96
  parallel (~> 1.10)
97
- parser (>= 2.7.0.1)
97
+ parser (>= 2.7.1.1)
98
98
  rainbow (>= 2.2.2, < 4.0)
99
99
  regexp_parser (>= 1.7)
100
100
  rexml
101
- rubocop-ast (>= 0.0.3)
101
+ rubocop-ast (>= 0.1.0, < 1.0)
102
102
  ruby-progressbar (~> 1.7)
103
103
  unicode-display_width (>= 1.4.0, < 2.0)
104
- rubocop-ast (0.0.3)
104
+ rubocop-ast (0.2.0)
105
105
  parser (>= 2.7.0.1)
106
106
  ruby-progressbar (1.10.1)
107
107
  ruby2_keywords (0.0.2)
@@ -111,7 +111,7 @@ GEM
111
111
  thread_safe (~> 0.1)
112
112
  unicode-display_width (1.7.0)
113
113
  uri_template (0.7.0)
114
- zeitwerk (2.3.0)
114
+ zeitwerk (2.4.0)
115
115
 
116
116
  PLATFORMS
117
117
  ruby
data/README.md CHANGED
@@ -36,7 +36,6 @@ Options and their defaults:
36
36
  | Name | Possible values | Description | Default
37
37
  |:---|---|---|---|
38
38
  |`spec:`| | The spec loaded via `OpenapiFirst.load` ||
39
- | `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)
40
39
  | `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)
41
40
 
42
41
  ## OpenapiFirst::RequestValidation
@@ -143,7 +142,7 @@ Instead of composing these middlewares yourself you can use `OpenapiFirst.app`.
143
142
  module Pets
144
143
  def self.find_pet(params, res)
145
144
  {
146
- id: params['id'],
145
+ id: params[:id],
147
146
  name: 'Oscar'
148
147
  }
149
148
  end
@@ -151,7 +150,12 @@ end
151
150
 
152
151
  # In config.ru:
153
152
  require 'openapi_first'
154
- run OpenapiFirst.app('./openapi/openapi.yaml', namespace: Pets)
153
+ run OpenapiFirst.app(
154
+ './openapi/openapi.yaml',
155
+ namespace: Pets,
156
+ response_validation: ENV['RACK_ENV'] == 'test',
157
+ router_raise_error: ENV['RACK_ENV'] == 'test'
158
+ )
155
159
  ```
156
160
 
157
161
  The above will use the mentioned Rack middlewares to:
@@ -160,6 +164,17 @@ The above will use the mentioned Rack middlewares to:
160
164
  - Map the request to a method call `Pets.find_pet` based on the `operationId` in the API description
161
165
  - Set the response content type according to your spec (here with the default status code `200`)
162
166
 
167
+ ### Options and their defaults:
168
+
169
+ | Name | Possible values | Description | Default
170
+ |:---|---|---|---|
171
+ | `spec_path` || A filepath to an OpenAPI definition file. |
172
+ | `namespace:` || A class or module where to find the handler methods.|
173
+ | `response_validation:` | `true`, `false` | If set to true it raises an exception if the response is invalid. This is useful during testing. | `false`
174
+ | `router_raise_error:` | `true`, `false` | If set to true it raises an exception (subclass of `OpenapiFirst::Error` when a request path/method is not specified. This is useful during testing. | `false`
175
+ | `request_validation_raise_error:` | `true`, `false` | If set to true it raises an exception (subclass of `OpenapiFirst::Error` when a request is not valid. | `false`
176
+
177
+
163
178
  Handler functions (`find_pet`) are called with two arguments:
164
179
 
165
180
  - `params` - Holds the parsed request body, filtered query params and path parameters
@@ -190,7 +205,7 @@ OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
190
205
 
191
206
  ## Manual response validation
192
207
 
193
- Response validation is useful to make sure your app responds as described in your API description. You usually do this in your tests using [rack-test](https://github.com/rack-test/rack-test).
208
+ Instead of using the ResponseValidation middleware you can validate the response in your test manually via [rack-test](https://github.com/rack-test/rack-test) and ResponseValidator.
194
209
 
195
210
  ```ruby
196
211
  # In your test (rspec example):
@@ -198,7 +213,8 @@ require 'openapi_first'
198
213
  spec = OpenapiFirst.load('petstore.yaml')
199
214
  validator = OpenapiFirst::ResponseValidator.new(spec)
200
215
 
201
- expect(validator.validate(last_request, last_response).errors).to be_empty
216
+ # This will raise an exception if it found an error
217
+ validator.validate(last_request, last_response)
202
218
  ```
203
219
 
204
220
  ## Handling only certain paths
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- openapi_first (0.12.0.alpha2)
4
+ openapi_first (0.12.4)
5
5
  deep_merge (>= 1.2.1)
6
6
  hanami-router (~> 2.0.alpha3)
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.1)
16
+ activesupport (6.0.3.2)
17
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
18
18
  i18n (>= 0.7, < 2)
19
19
  minitest (~> 5.1)
@@ -25,13 +25,13 @@ GEM
25
25
  benchmark-memory (0.1.2)
26
26
  memory_profiler (~> 0.9)
27
27
  builder (3.2.4)
28
- committee (4.0.0)
28
+ committee (4.1.0)
29
29
  json_schema (~> 0.14, >= 0.14.3)
30
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)
34
- dry-configurable (0.11.5)
34
+ dry-configurable (0.11.6)
35
35
  concurrent-ruby (~> 1.0)
36
36
  dry-core (~> 0.4, >= 0.4.7)
37
37
  dry-equalizer (~> 0.2)
@@ -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.3)
58
+ grape (1.4.0)
59
59
  activesupport
60
60
  builder
61
61
  dry-types (>= 1.1)
@@ -72,10 +72,10 @@ GEM
72
72
  transproc (~> 1.0)
73
73
  hansi (0.2.0)
74
74
  hash-deep-merge (0.1.1)
75
- i18n (1.8.2)
75
+ i18n (1.8.4)
76
76
  concurrent-ruby (~> 1.0)
77
- json_schema (0.20.8)
78
- json_schemer (0.2.11)
77
+ json_schema (0.20.9)
78
+ json_schemer (0.2.12)
79
79
  ecma-re-validator (~> 0.2)
80
80
  hana (~> 1.3)
81
81
  regexp_parser (~> 1.5)
@@ -83,7 +83,7 @@ GEM
83
83
  memory_profiler (0.9.14)
84
84
  mini_portile2 (2.4.0)
85
85
  minitest (5.14.1)
86
- multi_json (1.14.1)
86
+ multi_json (1.15.0)
87
87
  mustermann (1.1.1)
88
88
  ruby2_keywords (~> 0.0.1)
89
89
  mustermann-contrib (1.1.1)
@@ -91,7 +91,7 @@ GEM
91
91
  mustermann (= 1.1.1)
92
92
  mustermann-grape (1.0.1)
93
93
  mustermann (>= 1.0.0)
94
- nokogiri (1.10.9)
94
+ nokogiri (1.10.10)
95
95
  mini_portile2 (~> 2.4.0)
96
96
  oas_parser (0.25.1)
97
97
  activesupport (>= 4.0.0)
@@ -103,7 +103,7 @@ GEM
103
103
  nokogiri
104
104
  openapi_parser (0.11.2)
105
105
  public_suffix (4.0.5)
106
- rack (2.2.2)
106
+ rack (2.2.3)
107
107
  rack-accept (0.4.5)
108
108
  rack (>= 0.4)
109
109
  rack-protection (2.0.8.1)
@@ -125,7 +125,7 @@ GEM
125
125
  tzinfo (1.2.7)
126
126
  thread_safe (~> 0.1)
127
127
  uri_template (0.7.0)
128
- zeitwerk (2.3.0)
128
+ zeitwerk (2.4.0)
129
129
 
130
130
  PLATFORMS
131
131
  ruby
@@ -5,7 +5,7 @@ require 'openapi_first'
5
5
 
6
6
  namespace = Module.new do
7
7
  def self.find_thing(params, _res)
8
- { hello: 'world', id: params.fetch('id') }
8
+ { hello: 'world', id: params.fetch(:id) }
9
9
  end
10
10
 
11
11
  def self.find_things(_params, _res)
@@ -13,9 +13,10 @@ module Web
13
13
  end
14
14
 
15
15
  oas_path = File.absolute_path('./openapi.yaml', __dir__)
16
- pp OpenapiFirst.env == 'test'
16
+
17
17
  App = OpenapiFirst.app(
18
18
  oas_path,
19
19
  namespace: Web,
20
- raise_error: OpenapiFirst.env == 'test'
20
+ router_raise_error: OpenapiFirst.env == 'test',
21
+ response_validation: OpenapiFirst.env == 'test'
21
22
  )
@@ -31,14 +31,39 @@ module OpenapiFirst
31
31
  Definition.new(parsed)
32
32
  end
33
33
 
34
- def self.app(spec, namespace:, raise_error: false)
34
+ def self.app(
35
+ spec,
36
+ namespace:,
37
+ router_raise_error: false,
38
+ request_validation_raise_error: false,
39
+ response_validation: false
40
+ )
35
41
  spec = OpenapiFirst.load(spec) if spec.is_a?(String)
36
- App.new(nil, spec, namespace: namespace, raise_error: raise_error)
42
+ App.new(
43
+ nil,
44
+ spec,
45
+ namespace: namespace,
46
+ router_raise_error: router_raise_error,
47
+ request_validation_raise_error: request_validation_raise_error,
48
+ response_validation: response_validation
49
+ )
37
50
  end
38
51
 
39
- def self.middleware(spec, namespace:, raise_error: false)
52
+ def self.middleware(
53
+ spec,
54
+ namespace:,
55
+ router_raise_error: false,
56
+ request_validation_raise_error: false,
57
+ response_validation: false
58
+ )
40
59
  spec = OpenapiFirst.load(spec) if spec.is_a?(String)
41
- AppWithOptions.new(spec, namespace: namespace, raise_error: raise_error)
60
+ AppWithOptions.new(
61
+ spec,
62
+ namespace: namespace,
63
+ router_raise_error: router_raise_error,
64
+ request_validation_raise_error: request_validation_raise_error,
65
+ response_validation: response_validation
66
+ )
42
67
  end
43
68
 
44
69
  class AppWithOptions
@@ -55,9 +80,10 @@ module OpenapiFirst
55
80
  class Error < StandardError; end
56
81
  class NotFoundError < Error; end
57
82
  class NotImplementedError < RuntimeError; end
58
- class ResponseCodeNotFoundError < Error; end
59
- class ResponseMediaTypeNotFoundError < Error; end
60
- class ResponseBodyInvalidError < Error; end
83
+ class ResponseInvalid < Error; end
84
+ class ResponseCodeNotFoundError < ResponseInvalid; end
85
+ class ResponseContentTypeNotFoundError < ResponseInvalid; end
86
+ class ResponseBodyInvalidError < ResponseInvalid; end
61
87
 
62
88
  class RequestInvalidError < Error
63
89
  def initialize(serialized_errors)
@@ -5,12 +5,19 @@ require 'logger'
5
5
 
6
6
  module OpenapiFirst
7
7
  class App
8
- def initialize(parent_app, spec, namespace:, raise_error:)
8
+ def initialize( # rubocop:disable Metrics/ParameterLists
9
+ parent_app,
10
+ spec,
11
+ namespace:,
12
+ router_raise_error: false,
13
+ request_validation_raise_error: false,
14
+ response_validation: false
15
+ )
9
16
  @stack = Rack::Builder.app do
10
17
  freeze_app
11
- use OpenapiFirst::Router, spec: spec, raise_error: raise_error, parent_app: parent_app
12
- use OpenapiFirst::RequestValidation, raise_error: raise_error
13
- use OpenapiFirst::ResponseValidation if raise_error
18
+ use OpenapiFirst::Router, spec: spec, raise_error: router_raise_error, parent_app: parent_app
19
+ use OpenapiFirst::RequestValidation, raise_error: request_validation_raise_error
20
+ use OpenapiFirst::ResponseValidation if response_validation
14
21
  run OpenapiFirst::Responder.new(
15
22
  spec: spec,
16
23
  namespace: namespace
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'forwardable'
4
+ require 'json_schemer'
4
5
  require_relative 'utils'
5
6
  require_relative 'response_object'
6
7
 
@@ -25,6 +26,10 @@ module OpenapiFirst
25
26
  @parameters_json_schema ||= build_parameters_json_schema
26
27
  end
27
28
 
29
+ def parameters_schema
30
+ @parameters_schema ||= parameters_json_schema && JSONSchemer.schema(parameters_json_schema)
31
+ end
32
+
28
33
  def content_type_for(status)
29
34
  content = response_for(status)['content']
30
35
  content.keys[0] if content
@@ -34,14 +39,22 @@ module OpenapiFirst
34
39
  content = response_for(status)['content']
35
40
  return if content.nil? || content.empty?
36
41
 
37
- media_type = content[content_type]
42
+ raise ResponseInvalid, "Response has no content-type for '#{name}'" unless content_type
43
+
44
+ media_type = find_content_for_content_type(content, content_type)
38
45
  unless media_type
39
- message = "Response content type not found: '#{content_type}' for '#{name}'"
40
- raise ResponseMediaTypeNotFoundError, message
46
+ message = "Response content type not found '#{content_type}' for '#{name}'"
47
+ raise ResponseContentTypeNotFoundError, message
41
48
  end
42
49
  media_type['schema']
43
50
  end
44
51
 
52
+ def request_body_schema_for(request_content_type)
53
+ content = @operation.request_body.content
54
+ media_type = find_content_for_content_type(content, request_content_type)
55
+ media_type&.fetch('schema', nil)
56
+ end
57
+
45
58
  def response_for(status)
46
59
  @operation.response_by_code(status.to_s, use_default: true).raw
47
60
  rescue OasParser::ResponseCodeNotFound
@@ -55,6 +68,13 @@ module OpenapiFirst
55
68
 
56
69
  private
57
70
 
71
+ def find_content_for_content_type(content, request_content_type)
72
+ content.fetch(request_content_type) do |_|
73
+ type = request_content_type.split(';')[0]
74
+ content[type] || content["#{type.split('/')[0]}/*"] || content['*/*']
75
+ end
76
+ end
77
+
58
78
  def build_parameters_json_schema
59
79
  return unless @operation.parameters&.any?
60
80
 
@@ -50,32 +50,32 @@ module OpenapiFirst
50
50
 
51
51
  parsed_request_body = parse_request_body!(body)
52
52
  errors = validate_json_schema(schema, parsed_request_body)
53
- halt(error_response(400, serialize_request_body_errors(errors))) if errors.any?
53
+ halt_with_error(400, serialize_request_body_errors(errors)) if errors.any?
54
54
  env[INBOX].merge! env[REQUEST_BODY] = parsed_request_body
55
55
  end
56
56
 
57
57
  def parse_request_body!(body)
58
- MultiJson.load(body)
58
+ MultiJson.load(body, symbolize_keys: true)
59
59
  rescue MultiJson::ParseError => e
60
60
  err = { title: 'Failed to parse body as JSON' }
61
61
  err[:detail] = e.cause unless ENV['RACK_ENV'] == 'production'
62
- halt(error_response(400, [err]))
62
+ halt_with_error(400, [err])
63
63
  end
64
64
 
65
65
  def validate_request_content_type!(content_type, operation)
66
66
  return if operation.request_body.content[content_type]
67
67
 
68
- halt(error_response(415))
68
+ halt_with_error(415)
69
69
  end
70
70
 
71
71
  def validate_request_body_presence!(body, operation)
72
72
  return unless operation.request_body.required && body.empty?
73
73
 
74
- halt(error_response(415, 'Request body is required'))
74
+ halt_with_error(415, 'Request body is required')
75
75
  end
76
76
 
77
77
  def validate_json_schema(schema, object)
78
- JSONSchemer.schema(schema).validate(object)
78
+ schema.validate(Utils.deep_stringify(object))
79
79
  end
80
80
 
81
81
  def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
@@ -85,10 +85,10 @@ module OpenapiFirst
85
85
  }
86
86
  end
87
87
 
88
- def error_response(status, errors = [default_error(status)])
88
+ def halt_with_error(status, errors = [default_error(status)])
89
89
  raise RequestInvalidError, errors if @raise
90
90
 
91
- Rack::Response.new(
91
+ halt Rack::Response.new(
92
92
  MultiJson.dump(errors: errors),
93
93
  status,
94
94
  Rack::CONTENT_TYPE => 'application/vnd.api+json'
@@ -98,7 +98,9 @@ module OpenapiFirst
98
98
  def request_body_schema(content_type, operation)
99
99
  return unless operation
100
100
 
101
- operation.request_body.content[content_type]&.fetch('schema')
101
+ schema = operation.request_body_schema_for(content_type)
102
+
103
+ JSONSchemer.schema(schema) if schema
102
104
  end
103
105
 
104
106
  def serialize_request_body_errors(validation_errors)
@@ -116,8 +118,11 @@ module OpenapiFirst
116
118
  return unless json_schema
117
119
 
118
120
  params = filtered_params(json_schema, params)
119
- errors = JSONSchemer.schema(json_schema).validate(params)
120
- halt error_response(400, serialize_query_parameter_errors(errors)) if errors.any?
121
+ errors = validate_json_schema(
122
+ operation.parameters_schema,
123
+ params
124
+ )
125
+ halt_with_error(400, serialize_query_parameter_errors(errors)) if errors.any?
121
126
  env[PARAMETERS] = params
122
127
  env[INBOX].merge! params
123
128
  end
@@ -125,7 +130,8 @@ module OpenapiFirst
125
130
  def filtered_params(json_schema, params)
126
131
  json_schema['properties']
127
132
  .each_with_object({}) do |key_value, result|
128
- parameter_name, schema = key_value
133
+ parameter_name = key_value[0].to_sym
134
+ schema = key_value[1]
129
135
  next unless params.key?(parameter_name)
130
136
 
131
137
  value = params[parameter_name]
@@ -17,30 +17,24 @@ module OpenapiFirst
17
17
  operation = env[OPERATION]
18
18
  return @app.call(env) unless operation
19
19
 
20
- status, headers, body = @app.call(env)
20
+ response = @app.call(env)
21
+ validate(response, operation)
22
+ response
23
+ end
24
+
25
+ def validate(response, operation)
26
+ status, headers, body = response.to_a
27
+ return validate_status_only(operation, status) if status == 204
28
+
21
29
  content_type = headers[Rack::CONTENT_TYPE]
22
30
  response_schema = operation.response_schema_for(status, content_type)
23
31
  validate_response_body(response_schema, body) if response_schema
24
-
25
- [status, headers, body]
26
32
  end
27
33
 
28
34
  private
29
35
 
30
- def halt(status, body = '')
31
- throw :halt, [status, {}, body]
32
- end
33
-
34
- def error(message)
35
- { title: message }
36
- end
37
-
38
- def error_response(status, errors)
39
- Rack::Response.new(
40
- MultiJson.dump(errors: errors),
41
- status,
42
- Rack::CONTENT_TYPE => 'application/vnd.api+json'
43
- ).finish
36
+ def validate_status_only(operation, status)
37
+ operation.response_for(status)
44
38
  end
45
39
 
46
40
  def validate_response_body(schema, response)
@@ -48,7 +42,7 @@ module OpenapiFirst
48
42
  response.each { |chunk| full_body << chunk }
49
43
  data = full_body.empty? ? {} : load_json(full_body)
50
44
  errors = JSONSchemer.schema(schema).validate(data).to_a.map do |error|
51
- format_error(error)
45
+ error_message_for(error)
52
46
  end
53
47
  raise ResponseBodyInvalidError, errors.join(', ') if errors.any?
54
48
  end
@@ -59,7 +53,7 @@ module OpenapiFirst
59
53
  string
60
54
  end
61
55
 
62
- def format_error(error)
56
+ def error_message_for(error)
63
57
  err = ValidationFormat.error_details(error)
64
58
  [err[:title], error['data_pointer'], err[:detail]].compact.join(' ')
65
59
  end
@@ -10,57 +10,13 @@ module OpenapiFirst
10
10
  def initialize(spec)
11
11
  @spec = spec
12
12
  @router = Router.new(->(_env) {}, spec: spec, raise_error: true)
13
+ @response_validation = ResponseValidation.new(->(response) { response.to_a })
13
14
  end
14
15
 
15
16
  def validate(request, response)
16
- errors = validation_errors(request, response)
17
- Validation.new(errors || [])
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
26
- Validation.new([e.message])
27
- end
28
-
29
- private
30
-
31
- def validation_errors(request, response)
32
- content = response_for(request, response)&.fetch('content', nil)
33
- return unless content
34
-
35
- content_type = content[response.content_type]
36
- return ["Content type not found: '#{response.content_type}'"] unless content_type
37
-
38
- response_schema = content_type['schema']
39
- return unless response_schema
40
-
41
- response_data = MultiJson.load(response.body)
42
- validate_json_schema(response_schema, response_data)
43
- end
44
-
45
- def validate_json_schema(schema, data)
46
- JSONSchemer.schema(schema).validate(data).to_a.map do |error|
47
- format_error(error)
48
- end
49
- end
50
-
51
- def format_error(error)
52
- ValidationFormat.error_details(error)
53
- .merge!(
54
- data_pointer: error['data_pointer'],
55
- schema_pointer: error['schema_pointer']
56
- )
57
- end
58
-
59
- def response_for(request, response)
60
17
  env = request.env.dup
61
18
  @router.call(env)
62
- operation = env[OPERATION]
63
- operation&.response_for(response.status)
19
+ @response_validation.validate(response, env[OPERATION])
64
20
  end
65
21
  end
66
22
  end
@@ -6,58 +6,47 @@ require_relative 'utils'
6
6
 
7
7
  module OpenapiFirst
8
8
  class Router
9
- NOT_FOUND = Rack::Response.new('', 404).finish.freeze
10
- DEFAULT_NOT_FOUND_APP = ->(_env) { NOT_FOUND }
11
-
12
9
  def initialize(
13
10
  app,
14
11
  spec:,
15
12
  raise_error: false,
16
- parent_app: nil,
17
- not_found: nil
13
+ parent_app: nil
18
14
  )
19
15
  @app = app
20
16
  @parent_app = parent_app
21
17
  @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
18
  @filepath = spec.filepath
28
19
  @router = build_router(spec.operations)
29
20
  end
30
21
 
31
22
  def call(env)
32
23
  env[OPERATION] = nil
33
- route = find_route(env)
34
- return route.call(env) if route.routable?
24
+ response = call_router(env)
25
+ if env[OPERATION].nil?
26
+ return @parent_app.call(env) if @parent_app # This should only happen if used via OpenapiFirst.middlware
35
27
 
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
28
+ raise_error(env) if @raise
40
29
  end
41
- return @parent_app.call(env) if @parent_app
42
-
43
- @failure_app.call(env)
30
+ response
44
31
  end
45
32
 
46
- private
33
+ ORIGINAL_PATH = 'openapi_first.path_info'
47
34
 
48
- def find_failure_app(option)
49
- return DEFAULT_NOT_FOUND_APP if option.nil?
50
- return @app if option == :continue
35
+ private
51
36
 
52
- option if option.respond_to?(:call)
37
+ def raise_error(env)
38
+ req = Rack::Request.new(env)
39
+ msg = "Could not find definition for #{req.request_method} '#{req.path}' in API description #{@filepath}"
40
+ raise NotFoundError, msg
53
41
  end
54
42
 
55
- def find_route(env)
56
- original_path_info = env[Rack::PATH_INFO]
43
+ def call_router(env)
44
+ # Changing and restoring PATH_INFO is needed, because Hanami::Router does not respect existing script_path
45
+ env[ORIGINAL_PATH] = env[Rack::PATH_INFO]
57
46
  env[Rack::PATH_INFO] = Rack::Request.new(env).path
58
- @router.recognize(env)
47
+ @router.call(env)
59
48
  ensure
60
- env[Rack::PATH_INFO] = original_path_info
49
+ env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
61
50
  end
62
51
 
63
52
  def build_router(operations) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
@@ -73,7 +62,8 @@ module OpenapiFirst
73
62
  normalized_path,
74
63
  to: lambda do |env|
75
64
  env[OPERATION] = operation
76
- env[PARAMETERS] = Utils.deep_stringify(env['router.params'])
65
+ env[PARAMETERS] = env['router.params']
66
+ env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH)
77
67
  @app.call(env)
78
68
  end
79
69
  )
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.12.0.alpha2'
4
+ VERSION = '0.12.4'
5
5
  end
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.12.0.alpha2
4
+ version: 0.12.4
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-09 00:00:00.000000000 Z
11
+ date: 2020-07-22 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: 1.3.1
241
+ version: '0'
242
242
  requirements: []
243
243
  rubygems_version: 3.1.2
244
244
  signing_key: