openapi_first 3.0.1 → 3.1.0.beta1

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: 6f2547655bd57c98d6619ce62242b3f4b65177c8d8f228781dc55f469a79b51b
4
- data.tar.gz: 8f644a212e33370f6cd077469fecee5214c34f3b636658170ad3cae45eccc3df
3
+ metadata.gz: 5d07efa6286183d4d90a9126e3b5082e27fad9efa966f9aa7623ca280b51c6f8
4
+ data.tar.gz: 8c6e34a8dffd1b1dd7f8e71cfbb85ded8dd260d2f96631c8e7b91944ddb4033b
5
5
  SHA512:
6
- metadata.gz: 4fa6acbbb24462294ad5abfc31db63f3ceefbb1c8aa9edf5634a9400ff8f69e84e3fff0fe713a23a0afe71db696a1c080597617be449f3ee67607668e73fdd40
7
- data.tar.gz: 28f991a4401b32c122cb785ca616fb0de70935067be6ace0dcfa8fbca448b164cb2a84e4118264b2aa539d8ccf4528e2891981cdb9d58310b211a1bffc4ec652
6
+ metadata.gz: f60e8101c9a716ae9dbaf27dc9a815d6c44988f65e7d3cef73d93630f668ca664d13f1a0d356d777a6ba75dd0888a3baf44ad5a28716f4d93a02556c331bf46f
7
+ data.tar.gz: d9161ef397b3f8974f2540d37b12bf07d1b6ff2acb55413504a8fd541c3e91bde36a34712609ef0ef48f93cda59fb6898f4856a0a8c67ce8923351d06ef61709
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 3.1.0.beta1
6
+
7
+ ### openapi_first/test
8
+
9
+ #### Changed
10
+ - Test now raises `OpenapiFirst::Test::UnknownQueryParameterError` when it sees unknown query parameters. Note that `OpenapiFirst` ("core") still allows unknown query parameters.
11
+ - Test does not track requests/responses unless the OAD was registered via Test.register (or OpenapiFirst.register)
12
+
5
13
  ## 3.0.1
6
14
  - Add missing gem dependency "drb", which is no longer installed by default with newer rubies. This is used in openapi_first/test to make parallel tests work.
7
15
 
data/README.md CHANGED
@@ -243,7 +243,7 @@ Or you can ignore all unknown response status:
243
243
 
244
244
  ```ruby
245
245
  OpenapiFirst::Test.setup do |test|
246
- test.ignore_all_unknown_status = true
246
+ test.ignore_unknown_response_status = true
247
247
  end
248
248
  ```
249
249
 
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ ParsedRequest = Data.define(:path, :query, :headers, :body, :cookies)
5
+ end
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'request_parser'
3
+ require 'openapi_parameters'
4
+ require_relative 'parsed_request'
4
5
  require_relative 'request_validator'
5
6
  require_relative 'validated_request'
7
+ require_relative 'request_body_parsers'
6
8
 
7
9
  module OpenapiFirst
8
10
  # Represents one request definition of an OpenAPI description.
@@ -10,8 +12,7 @@ module OpenapiFirst
10
12
  # An 3.x Operation object can accept multiple requests, because it can handle multiple content-types.
11
13
  # This class represents one of those requests.
12
14
  class Request
13
- # rubocop:disable Metrics/MethodLength
14
- def initialize(path:, request_method:, operation_object:,
15
+ def initialize(path:, request_method:, operation_object:, # rubocop:disable Metrics/MethodLength
15
16
  parameters:, content_type:, content_schema:, required_body:, key:)
16
17
  @path = path
17
18
  @request_method = request_method
@@ -20,13 +21,11 @@ module OpenapiFirst
20
21
  @operation = operation_object
21
22
  @allow_empty_content = content_type.nil? || required_body == false
22
23
  @key = key
23
- @request_parser = RequestParser.new(
24
- query_parameters: parameters.query,
25
- path_parameters: parameters.path,
26
- header_parameters: parameters.header,
27
- cookie_parameters: parameters.cookie,
28
- content_type:
29
- )
24
+ @query_parser = parameters.query&.then { |params| OpenapiParameters::Query.new(params) }
25
+ @path_parser = parameters.path&.then { |params| OpenapiParameters::Path.new(params) }
26
+ @headers_parser = parameters.header&.then { |params| OpenapiParameters::Header.new(params) }
27
+ @cookies_parser = parameters.cookie&.then { |params| OpenapiParameters::Cookie.new(params) }
28
+ @body_parsers = RequestBodyParsers[content_type] if content_type
30
29
  @validator = RequestValidator.new(
31
30
  content_schema:,
32
31
  required_request_body: required_body == true,
@@ -35,10 +34,11 @@ module OpenapiFirst
35
34
  header_schema: parameters.header_schema,
36
35
  cookie_schema: parameters.cookie_schema
37
36
  )
37
+ @parameters = parameters
38
38
  end
39
- # rubocop:enable Metrics/MethodLength
40
39
 
41
- attr_reader :content_type, :content_schema, :operation, :request_method, :path, :key
40
+ attr_reader :content_type, :content_schema, :operation, :request_method, :path, :key, :query_schema, :parameters
41
+ private attr_reader :query_parser
42
42
 
43
43
  def allow_empty_content?
44
44
  @allow_empty_content
@@ -47,15 +47,33 @@ module OpenapiFirst
47
47
  def validate(request, route_params:)
48
48
  parsed_request = nil
49
49
  error = catch FAILURE do
50
- parsed_request = @request_parser.parse(request, route_params:)
50
+ parsed_request = parse_request(request, route_params:)
51
51
  @validator.call(parsed_request)
52
52
  nil
53
53
  end
54
- ValidatedRequest.new(request, parsed_request:, error:, request_definition: self)
54
+ ValidatedRequest.new(request, parsed_request:, error:, request_definition: self, query_parser:)
55
55
  end
56
56
 
57
57
  def operation_id
58
58
  @operation['operationId']
59
59
  end
60
+
61
+ private
62
+
63
+ def parse_request(request, route_params:)
64
+ ParsedRequest.new(
65
+ path: @path_parser&.unpack(route_params),
66
+ query: parse_query(request.env[Rack::QUERY_STRING]),
67
+ headers: @headers_parser&.unpack_env(request.env),
68
+ cookies: @cookies_parser&.unpack(request.env[Rack::HTTP_COOKIE]),
69
+ body: @body_parsers&.call(request)
70
+ )
71
+ end
72
+
73
+ def parse_query(query_string)
74
+ @query_parser&.unpack(query_string)
75
+ rescue OpenapiParameters::InvalidParameterError
76
+ Failure.fail!(:invalid_query, message: 'Invalid query parameter.')
77
+ end
60
78
  end
61
79
  end
@@ -5,7 +5,7 @@ require_relative 'registry'
5
5
 
6
6
  module OpenapiFirst
7
7
  # Test integration
8
- module Test
8
+ module Test # rubocop:disable Metrics/ModuleLength
9
9
  autoload :Coverage, 'openapi_first/test/coverage'
10
10
  autoload :Methods, 'openapi_first/test/methods'
11
11
  autoload :Observe, 'openapi_first/test/observe'
@@ -13,6 +13,7 @@ module OpenapiFirst
13
13
  extend Registry
14
14
 
15
15
  class CoverageError < Error; end
16
+ class UnknownQueryParameterError < Error; end
16
17
 
17
18
  # Inject request/response validation in a rack app class
18
19
  def self.observe(app, api: :default)
@@ -33,6 +34,11 @@ module OpenapiFirst
33
34
  @configuration ||= Configuration.new
34
35
  end
35
36
 
37
+ def self.registered?(oad)
38
+ key = oad.key
39
+ definitions.any? { |(_name, registered)| registered.key == key }
40
+ end
41
+
36
42
  # Sets up OpenAPI test coverage and OAD registration.
37
43
  # @yieldparam [OpenapiFirst::Test::Configuration] configuration A configuration to setup test integration
38
44
  def self.setup
@@ -99,12 +105,16 @@ module OpenapiFirst
99
105
 
100
106
  OpenapiFirst.configure do |config|
101
107
  @after_request_validation = config.after_request_validation do |validated_request, oad|
108
+ next unless registered?(oad)
102
109
  raise validated_request.error.exception if raise_request_error?(validated_request)
103
110
 
111
+ check_unknown_query_parameters(validated_request)
112
+
104
113
  Coverage.track_request(validated_request, oad)
105
114
  end
106
115
 
107
116
  @after_response_validation = config.after_response_validation do |validated_response, rack_request, oad|
117
+ next unless registered?(oad)
108
118
  if validated_response.invalid? && raise_response_error?(validated_response)
109
119
  raise validated_response.error.exception
110
120
  end
@@ -115,17 +125,6 @@ module OpenapiFirst
115
125
  @installed = true
116
126
  end
117
127
 
118
- def self.raise_request_error?(validated_request)
119
- return false if validated_request.valid?
120
- return false if validated_request.known?
121
-
122
- !configuration.ignore_unknown_requests
123
- end
124
-
125
- def self.raise_response_error?(invalid_response)
126
- configuration.response_raise_error && !configuration.ignore_response?(invalid_response)
127
- end
128
-
129
128
  def self.uninstall
130
129
  configuration = OpenapiFirst.configuration
131
130
  configuration.after_request_validation.delete(@after_request_validation)
@@ -135,5 +134,38 @@ module OpenapiFirst
135
134
  @installed = nil
136
135
  @exit_handler = nil
137
136
  end
137
+
138
+ class << self
139
+ private
140
+
141
+ def check_unknown_query_parameters(validated_request)
142
+ return unless validated_request.known?
143
+
144
+ unknown_parameters = validated_request.unknown_query_parameters
145
+ return unless unknown_parameters
146
+
147
+ message = unknown_parameters_message(unknown_parameters, validated_request)
148
+ raise UnknownQueryParameterError, message
149
+ end
150
+
151
+ def unknown_parameters_message(unknown_parameters, validated_request)
152
+ s = 's' if many?(unknown_parameters)
153
+ list = unknown_parameters.keys.map(&:inspect).join(', ')
154
+ "Unknown query parameter#{s} #{list} for #{validated_request.fullpath}"
155
+ end
156
+
157
+ def raise_request_error?(validated_request)
158
+ return false if validated_request.valid?
159
+ return false if validated_request.known?
160
+
161
+ !configuration.ignore_unknown_requests
162
+ end
163
+
164
+ def many?(array) = array.length > 1
165
+
166
+ def raise_response_error?(invalid_response)
167
+ configuration.response_raise_error && !configuration.ignore_response?(invalid_response)
168
+ end
169
+ end
138
170
  end
139
171
  end
@@ -8,11 +8,12 @@ module OpenapiFirst
8
8
  class ValidatedRequest < SimpleDelegator
9
9
  extend Forwardable
10
10
 
11
- def initialize(original_request, error:, parsed_request: nil, request_definition: nil)
11
+ def initialize(original_request, error:, parsed_request: nil, request_definition: nil, query_parser: nil)
12
12
  super(original_request)
13
13
  @parsed_request = parsed_request
14
14
  @error = error
15
15
  @request_definition = request_definition
16
+ @query_parser = query_parser
16
17
  end
17
18
 
18
19
  # A Failure object if the request is invalid
@@ -31,6 +32,11 @@ module OpenapiFirst
31
32
  # @return [Hash] The raw OpenAPI 3 operation object
32
33
  def_delegator :request_definition, :operation
33
34
 
35
+ # @return [Hash] Query parameters and values that are not defined in the OpenAPI spec.
36
+ def unknown_query_parameters
37
+ @query_parser&.unknown_values(query_string)
38
+ end
39
+
34
40
  # Parsed path parameters
35
41
  # @return [Hash<String, anything>]
36
42
  def parsed_path_parameters = @parsed_request&.path || {}
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '3.0.1'
4
+ VERSION = '3.1.0.beta1'
5
5
  end
data/lib/openapi_first.rb CHANGED
@@ -47,8 +47,8 @@ module OpenapiFirst
47
47
  # @return [Class] The error response class
48
48
  def self.find_error_response(name)
49
49
  ERROR_RESPONSES.fetch(name) do
50
- raise "Unknown error response: #{name}. " /
51
- 'Register your error response class via `OpenapiFirst.register_error_response(name, klass)`. ' /
50
+ raise "Unknown error response: #{name}. " \
51
+ 'Register your error response class via `OpenapiFirst.register_error_response(name, klass)`. ' \
52
52
  "Registered error responses are: #{ERROR_RESPONSES.keys.join(', ')}."
53
53
  end
54
54
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi_first
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.1
4
+ version: 3.1.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
@@ -63,7 +63,7 @@ dependencies:
63
63
  requirements:
64
64
  - - ">="
65
65
  - !ruby/object:Gem::Version
66
- version: 0.7.0
66
+ version: 0.10.0
67
67
  - - "<"
68
68
  - !ruby/object:Gem::Version
69
69
  version: '2.0'
@@ -73,7 +73,7 @@ dependencies:
73
73
  requirements:
74
74
  - - ">="
75
75
  - !ruby/object:Gem::Version
76
- version: 0.7.0
76
+ version: 0.10.0
77
77
  - - "<"
78
78
  - !ruby/object:Gem::Version
79
79
  version: '2.0'
@@ -121,11 +121,11 @@ files:
121
121
  - lib/openapi_first/json.rb
122
122
  - lib/openapi_first/middlewares/request_validation.rb
123
123
  - lib/openapi_first/middlewares/response_validation.rb
124
+ - lib/openapi_first/parsed_request.rb
124
125
  - lib/openapi_first/ref_resolver.rb
125
126
  - lib/openapi_first/registry.rb
126
127
  - lib/openapi_first/request.rb
127
128
  - lib/openapi_first/request_body_parsers.rb
128
- - lib/openapi_first/request_parser.rb
129
129
  - lib/openapi_first/request_validator.rb
130
130
  - lib/openapi_first/response.rb
131
131
  - lib/openapi_first/response_body_parsers.rb
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'openapi_parameters'
4
- require_relative 'request_body_parsers'
5
-
6
- module OpenapiFirst
7
- ParsedRequest = Data.define(:path, :query, :headers, :body, :cookies)
8
-
9
- # Parse a request
10
- class RequestParser
11
- def initialize(
12
- query_parameters:,
13
- path_parameters:,
14
- header_parameters:,
15
- cookie_parameters:,
16
- content_type:
17
- )
18
- @query_parser = OpenapiParameters::Query.new(query_parameters) if query_parameters
19
- @path_parser = OpenapiParameters::Path.new(path_parameters) if path_parameters
20
- @headers_parser = OpenapiParameters::Header.new(header_parameters) if header_parameters
21
- @cookies_parser = OpenapiParameters::Cookie.new(cookie_parameters) if cookie_parameters
22
- @body_parsers = RequestBodyParsers[content_type] if content_type
23
- end
24
-
25
- attr_reader :query, :path, :headers, :cookies
26
-
27
- def parse(request, route_params:)
28
- ParsedRequest.new(
29
- path: @path_parser&.unpack(route_params),
30
- query: parse_query(request.env[Rack::QUERY_STRING]),
31
- headers: @headers_parser&.unpack_env(request.env),
32
- cookies: @cookies_parser&.unpack(request.env[Rack::HTTP_COOKIE]),
33
- body: @body_parsers&.call(request)
34
- )
35
- end
36
-
37
- private
38
-
39
- def parse_query(query_string)
40
- @query_parser&.unpack(query_string)
41
- rescue OpenapiParameters::InvalidParameterError
42
- Failure.fail!(:invalid_query, message: 'Invalid query parameter.')
43
- end
44
- end
45
- end