openapi_first 4.0.0 → 4.1.0

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: 47dffdd527985dd188f08ea57ff1104f3e51203746175cf4a964c757af8ebd6e
4
- data.tar.gz: c67d1e706d644caadd9397bfdb2eef046317e2c4ac86bfd8e261b60ad7136bf7
3
+ metadata.gz: ac70785f7221fecd32d0a1408c8c12ee14ea75f5d45be6c9c1efce38e735d02e
4
+ data.tar.gz: 0cea3e400f9eed814fc9924c551893d78444920d99f7573d59426a7fa7a61068
5
5
  SHA512:
6
- metadata.gz: e5e16ce4c6c4756517e9bff345eb7182937b239be36a038444680f61b169790aef3c5d10c3c1d8b2be0e3332b72784a3722ba3ea99de0e7712f4c1e48284c0ec
7
- data.tar.gz: fa631d3327bbcf640645d2b762d00ac8baa4bf5c87276c9c00cc280e58d1985a419a64ccd4b8521f411f9d5f47442ae9efc31f0bf959b5a8d4f24fb8205de7a7
6
+ metadata.gz: 9c8883be1aad4a502ac4596ecb91765b855e2d515ec3e6ad11519a350ed43f29ae591645b71f7f7da709775cd1322c9dd4803f1474032bf5e8004abfd4b0eb51
7
+ data.tar.gz: 21e78cbcc5607436c1a32e108f786b0ddfc7ad434d2d103cee6f506eb90bef74d6dfcc4977ce748b612aeeab0c9e50c3a372c77da66a291275ead799bf677226
data/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 4.1.0
6
+
7
+ - Query and cookie parameter values with invalid encoding (like `?search=%C3`) produce a 400 response now. Before such a value passed validation or raised an `ArgumentError`. This applies to documented and undocumented query parameter, but only to documented cookie parameters.
8
+ - A query string with more parameters than Rack accepts (4096 by default) produces a 400 response now instead of raising `Rack::QueryParser::QueryLimitError`. See the [rack documentation](https://rack.github.io/rack/main/index.html#configuration) about how to configure this.
9
+
10
+ ## 4.0.1
11
+
12
+ - Fix: Reduced memory usage with large API descriptions. Schemas no longer each deep-copy (stringify keys) the whole OAD to build a root schema. This is a json_schemer specific optimization.
13
+
5
14
  ## 4.0.0
6
15
 
7
16
  This release has no stricter or less strict request validation. It changes mostly internal stuff and adds a Sinatra integration. It's a major version, but it should be safe to upgrade.
@@ -126,10 +126,10 @@ module OpenapiFirst
126
126
  cookie = build_parameters(grouped[:cookie])
127
127
  ParsedParameters.new(
128
128
  all: [*path, *query, *header, *cookie].freeze,
129
- path_parser: grouped[:path] && ParametersParser.new(path),
129
+ path_parser: grouped[:path] && ParametersParser.new(path, check_encoding: false),
130
130
  query_parser: QueryStringParser.new(query),
131
- header_parser: grouped[:header] && ParametersParser.new(header),
132
- cookie_parser: grouped[:cookie] && ParametersParser.new(cookie),
131
+ header_parser: grouped[:header] && ParametersParser.new(header, check_encoding: false),
132
+ cookie_parser: grouped[:cookie] && ParametersParser.new(cookie, check_encoding: true),
133
133
  path_schema: build_parameter_schema(grouped[:path]),
134
134
  query_schema: build_parameter_schema(grouped[:query]),
135
135
  header_schema: build_parameter_schema(grouped[:header]),
@@ -26,12 +26,20 @@ module OpenapiFirst
26
26
  if extname == '.json'
27
27
  ::JSON.parse(body)
28
28
  elsif ['.yaml', '.yml'].include?(extname)
29
- YAML.unsafe_load(body)
29
+ load_yaml(body, file_path)
30
30
  else
31
31
  body
32
32
  end
33
33
  end
34
34
  end
35
35
  end
36
+
37
+ private
38
+
39
+ def load_yaml(body, file_path)
40
+ Utils.deep_stringify_keys(YAML.unsafe_load(body))
41
+ rescue ::JSON::GeneratorError => e
42
+ raise Error, "Could not load #{file_path.inspect}: #{e.message}"
43
+ end
36
44
  end
37
45
  end
@@ -5,8 +5,10 @@ module OpenapiFirst
5
5
  # @visibility private
6
6
  class ParametersParser
7
7
  # @param parameters [Array<Parameter>]
8
- def initialize(parameters)
8
+ # @param check_encoding [Boolean] Raise Rack::Utils::InvalidParameterError if a value has an invalid encoding.
9
+ def initialize(parameters, check_encoding:)
9
10
  @parameters = parameters
11
+ @check_encoding = check_encoding
10
12
  end
11
13
 
12
14
  attr_reader :parameters
@@ -16,8 +18,18 @@ module OpenapiFirst
16
18
  parameters.each_with_object({}) do |parameter, result|
17
19
  next unless parameters_hash.key?(parameter.name)
18
20
 
19
- result[parameter.name] = parameter.unpack_and_convert(parameters_hash[parameter.name])
21
+ value = parameters_hash[parameter.name]
22
+ check_encoding!(value) if @check_encoding
23
+ result[parameter.name] = parameter.unpack_and_convert(value)
20
24
  end
21
25
  end
26
+
27
+ private
28
+
29
+ def check_encoding!(value)
30
+ return if !value.is_a?(::String) || value.valid_encoding?
31
+
32
+ raise Rack::Utils::InvalidParameterError, "invalid encoding (#{value.inspect})"
33
+ end
22
34
  end
23
35
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rack'
4
+ require 'rack/query_parser'
4
5
  require_relative 'parameters_parser'
5
6
 
6
7
  module OpenapiFirst
@@ -10,11 +11,15 @@ module OpenapiFirst
10
11
  DEEP_PROP = '\[([\w-]+)\]$'
11
12
  private_constant :DEEP_PROP
12
13
 
14
+ # Rack < 3.1 does not know about Rack::BadRequest
15
+ BAD_REQUEST = defined?(Rack::BadRequest) ? Rack::BadRequest : Rack::QueryParser::QueryLimitError
16
+ private_constant :BAD_REQUEST
17
+
13
18
  # @param parameters [Array<Parameter>]
14
19
  def initialize(parameters)
15
20
  @parameters = parameters
16
21
  @deep_object_parameters, flat_parameters = parameters.partition(&:deep_object?)
17
- @flat_parser = ParametersParser.new(flat_parameters)
22
+ @flat_parser = ParametersParser.new(flat_parameters, check_encoding: false)
18
23
  @deep_object_properties = {}
19
24
  @deep_object_regex = {}
20
25
  @deep_object_parameters.each do |parameter|
@@ -54,16 +59,23 @@ module OpenapiFirst
54
59
  return if unknown.empty?
55
60
 
56
61
  unknown
62
+ rescue Rack::Utils::InvalidParameterError
63
+ nil
57
64
  end
58
65
 
59
66
  private
60
67
 
61
68
  def parse_query(query_string)
62
69
  Rack::Utils.parse_query(query_string) do |string|
63
- Rack::Utils.unescape(string)
70
+ unescaped = Rack::Utils.unescape(string)
71
+ next unescaped if unescaped.valid_encoding?
72
+
73
+ raise Rack::Utils::InvalidParameterError, "invalid encoding (#{string})"
64
74
  rescue ArgumentError => e
65
75
  raise Rack::Utils::InvalidParameterError, e.message
66
76
  end
77
+ rescue BAD_REQUEST => e
78
+ raise Rack::Utils::InvalidParameterError, e.message
67
79
  end
68
80
 
69
81
  def parse_deep_object(parameter, parsed_query)
@@ -184,6 +184,17 @@ module OpenapiFirst
184
184
  vocabulary: { 'https://json-schema.org/draft/2020-12/vocab/core' => true }
185
185
  )
186
186
 
187
+ # This class is here to monkey-patch JSONSchemer::Schema when we are passing
188
+ # an instance to sub-schemas. It skips stringifying all keys of the passed object to
189
+ # allocate fewer objects. We don't have to stringify the keys, because they are already stringified upstream.
190
+ # @visibility private
191
+ class DocumentRootSchema < JSONSchemer::Schema
192
+ private
193
+
194
+ def deep_stringify_keys(value) = value
195
+ end
196
+ private_constant :DocumentRootSchema
197
+
187
198
  def initialize(value:, context:, base_uri:, options:)
188
199
  @value = value
189
200
  @context = context
@@ -197,7 +208,7 @@ module OpenapiFirst
197
208
 
198
209
  def schema
199
210
  @schema ||= begin
200
- root_schema = JSONSchemer::Schema.new(context, base_uri:, **options, meta_schema: DOCUMENT_META_SCHEMA)
211
+ root_schema = DocumentRootSchema.new(context, base_uri:, **options, meta_schema: DOCUMENT_META_SCHEMA)
201
212
  apply_dialect(root_schema)
202
213
  JSONSchemer::Schema.new(value, nil, root_schema, base_uri:, **options)
203
214
  end
@@ -61,6 +61,9 @@ module OpenapiFirst
61
61
  query, query_error = parse_query(request.env[Rack::QUERY_STRING])
62
62
  return [nil, query_error] if query_error
63
63
 
64
+ cookies = parse_cookies(request.env[Rack::HTTP_COOKIE])
65
+ return [nil, cookies] if cookies.is_a?(Failure)
66
+
64
67
  body = @body_parsers&.call(request)
65
68
  return [nil, body] if body.is_a?(Failure)
66
69
 
@@ -68,7 +71,7 @@ module OpenapiFirst
68
71
  path: @path_parser&.unpack(route_params),
69
72
  query:,
70
73
  headers: @header_parser&.unpack(RequestHeaders.new(request.env)),
71
- cookies: @cookie_parser&.unpack(Rack::Utils.parse_cookies_header(request.env[Rack::HTTP_COOKIE])),
74
+ cookies:,
72
75
  body:
73
76
  ), nil]
74
77
  end
@@ -79,6 +82,12 @@ module OpenapiFirst
79
82
  [nil, Failure.new(:invalid_query, message: 'Invalid query parameter.')]
80
83
  end
81
84
 
85
+ def parse_cookies(cookie_header)
86
+ @cookie_parser&.unpack(Rack::Utils.parse_cookies_header(cookie_header))
87
+ rescue Rack::Utils::InvalidParameterError
88
+ Failure.new(:invalid_cookie, message: 'Invalid cookie value.')
89
+ end
90
+
82
91
  def build_body_parser(content_type, encoding)
83
92
  RequestBodyParsers[content_type, { encoding: encoding || {} }]
84
93
  end
@@ -40,7 +40,7 @@ module OpenapiFirst
40
40
  def build_headers_parser(headers)
41
41
  return unless headers&.any?
42
42
 
43
- ParametersParser.new(headers.map(&:parameter))
43
+ ParametersParser.new(headers.map(&:parameter), check_encoding: false)
44
44
  end
45
45
  end
46
46
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ # @visibility private
5
+ module Utils
6
+ module_function
7
+
8
+ def deep_stringify_keys(contents) = ::JSON.parse(::JSON.generate(contents))
9
+ end
10
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '4.0.0'
4
+ VERSION = '4.1.0'
5
5
  end
data/lib/openapi_first.rb CHANGED
@@ -4,6 +4,7 @@ require_relative 'openapi_first/json'
4
4
  require_relative 'openapi_first/file_loader'
5
5
  require_relative 'openapi_first/errors'
6
6
  require_relative 'openapi_first/registry'
7
+ require_relative 'openapi_first/utils'
7
8
  require_relative 'openapi_first/configuration'
8
9
  require_relative 'openapi_first/child_configuration'
9
10
  require_relative 'openapi_first/definition'
@@ -82,7 +83,7 @@ module OpenapiFirst
82
83
  # @return [Definition]
83
84
  # TODO: This needs to work with unresolved contents as well
84
85
  def self.parse(contents, only: nil, filepath: nil, path_prefix: nil, &)
85
- contents = ::JSON.parse(::JSON.generate(contents)) # Deeply stringify keys, because of YAML. See https://github.com/ahx/openapi_first/issues/367
86
+ contents = Utils.deep_stringify_keys(contents) # Deeply stringify keys, because of YAML. See https://github.com/ahx/openapi_first/issues/367
86
87
  contents['paths'].filter!(&->(key, _) { only.call(key) }) if only
87
88
  Definition.new(contents, filepath, path_prefix, &)
88
89
  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: 4.0.0
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
@@ -154,6 +154,7 @@ files:
154
154
  - lib/openapi_first/test/observe.rb
155
155
  - lib/openapi_first/test/observer_middleware.rb
156
156
  - lib/openapi_first/test/plain_helpers.rb
157
+ - lib/openapi_first/utils.rb
157
158
  - lib/openapi_first/validated_request.rb
158
159
  - lib/openapi_first/validated_response.rb
159
160
  - lib/openapi_first/validators/multipart_request_body.rb