openapi_first 3.2.1 → 3.3.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: b961989b161d89284ee34052dad18d93190f6c844377fd5d3fdc30e61254ab60
4
- data.tar.gz: '08b6df291a7172842e698c78b56cb453e514bf2dee8915f7aded306e88dedadf'
3
+ metadata.gz: 7d164451de439181b88fc45c5735f564cc802f5f53e13e245d224ca474a12c03
4
+ data.tar.gz: c5adc0092bccf3de1248799ddde58bf77dce9c58858e588dd72e8c8ed4a5c6cd
5
5
  SHA512:
6
- metadata.gz: 56a10d73bf0a1f2625a663396c36ad569b558ff3204ded4aa5d378a85da72aef6194baee26cd92212f23ebb027fc4da75ef38d21c0196f3eeb681393ac81515c
7
- data.tar.gz: 59659f2434ff18dafabfd6703b12d67820f35b7e47016eb20338da3ad1e3217df94cf37d51a86d99803f9fa544e540bd3cfbfa7151b99e398ef87c90f6b58468
6
+ metadata.gz: ca4c5185152f49fe83977856d585f31ab1490f6b813a3f9878fc42e266f84dddc445baaf6865868e4a2b9c24f9ab7dccc2732ccc74f15aea43363a2f2b519e80
7
+ data.tar.gz: 43e0abb51e9d78fe86d0fb0903e372dbdfe480e73906148dd9a1da15261feb41d3614c3a6d337e11796baceff375c7ea887cbb8e7bbbb7098e6f00ec3825c5c8
data/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 3.3.0
6
+
7
+ - OpenapiFirst will now cache the contents of files that have been loaded. If you need to reload your OpenAPI definition for tests or server hot reloading, you can call `OpenapiFirst.clear_cache!`.
8
+ - Optimized `OpenapiFirst::Router#match` for faster path matching and reduced memory allocation.
9
+
5
10
  ## 3.2.1
6
11
 
7
12
  - Don't raise `UnknownQueryParameterError` if request is ignored in tests. Fixes [#441](https://github.com/ahx/openapi_first/issues/441).
@@ -3,7 +3,7 @@
3
3
  module OpenapiFirst
4
4
  # A failure object returned when validation or parsing of a request or response has failed.
5
5
  # This returned in ValidatedRequest#error and ValidatedResponse#error.
6
- class Failure
6
+ class Failure < Data.define(:type, :message, :errors) # rubocop:disable Style/DataInheritance
7
7
  TYPES = {
8
8
  not_found: [NotFoundError, 'Not found.'],
9
9
  method_not_allowed: [RequestInvalidError, 'Request method is not defined.'],
@@ -33,27 +33,25 @@ module OpenapiFirst
33
33
  # @param type [Symbol] See TYPES.keys
34
34
  # @param message [String] A generic error message
35
35
  # @param errors [Array<OpenapiFirst::Schema::ValidationError>]
36
- def initialize(type, message: nil, errors: nil)
36
+ def self.new(type, message: nil, errors: nil)
37
37
  unless TYPES.key?(type)
38
38
  raise ArgumentError,
39
39
  "type must be one of #{TYPES.keys} but was #{type.inspect}"
40
40
  end
41
-
42
- @type = type
43
- @message = message
44
- @errors = errors
41
+ super(type:, message:, errors:)
45
42
  end
46
43
 
47
- # @attr_reader [Symbol] type The type of the failure. See TYPES.keys.
44
+ # @method type [Symbol] type The type of the failure. See TYPES.keys.
48
45
  # Example: :invalid_body
49
- attr_reader :type
50
46
 
51
- # @attr_reader [Array<OpenapiFirst::Schema::ValidationError>] errors Schema validation errors
52
- attr_reader :errors
47
+ # @method errors [Array<OpenapiFirst::Schema::ValidationError>, nil] errors Schema validation errors
48
+
49
+ alias original_message message
50
+ private :original_message
53
51
 
54
52
  # A generic error message
55
53
  def message
56
- @message ||= exception_message
54
+ original_message || exception_message
57
55
  end
58
56
 
59
57
  def exception(context = nil)
@@ -63,7 +61,7 @@ module OpenapiFirst
63
61
  def exception_message
64
62
  _, message_prefix = TYPES.fetch(type)
65
63
 
66
- [message_prefix, @message || generate_message].compact.join(' ')
64
+ [message_prefix, original_message || generate_message].compact.join(' ')
67
65
  end
68
66
 
69
67
  private
@@ -6,17 +6,32 @@ require 'yaml'
6
6
  module OpenapiFirst
7
7
  # @!visibility private
8
8
  module FileLoader
9
+ @cache = {}
10
+ @mutex = Mutex.new
11
+
9
12
  module_function
10
13
 
11
14
  def load(file_path)
12
- raise FileNotFoundError, "File not found #{file_path.inspect}" unless File.exist?(file_path)
15
+ @cache[file_path] || @mutex.synchronize do
16
+ @cache[file_path] ||= begin
17
+ raise FileNotFoundError, "File not found #{file_path.inspect}" unless File.exist?(file_path)
18
+
19
+ body = File.read(file_path)
20
+ extname = File.extname(file_path)
13
21
 
14
- body = File.read(file_path)
15
- extname = File.extname(file_path)
16
- return ::JSON.parse(body) if extname == '.json'
17
- return YAML.unsafe_load(body) if ['.yaml', '.yml'].include?(extname)
22
+ if extname == '.json'
23
+ ::JSON.parse(body)
24
+ elsif ['.yaml', '.yml'].include?(extname)
25
+ YAML.unsafe_load(body)
26
+ else
27
+ body
28
+ end
29
+ end
30
+ end
31
+ end
18
32
 
19
- body
33
+ def clear_cache!
34
+ @mutex.synchronize { @cache.clear }
20
35
  end
21
36
  end
22
37
  end
@@ -25,7 +25,7 @@ module OpenapiFirst
25
25
  buffered_body = +''
26
26
 
27
27
  if rack_response.body.respond_to?(:each)
28
- rack_response.body.each { |chunk| buffered_body.to_s << chunk }
28
+ rack_response.body.each { |chunk| buffered_body << chunk }
29
29
  return buffered_body
30
30
  end
31
31
  rack_response.body
@@ -8,8 +8,10 @@ module OpenapiFirst
8
8
  return contents[nil] if content_type.nil? || content_type.empty?
9
9
 
10
10
  contents.fetch(content_type) do
11
- type = content_type.split(';')[0]
12
- contents[type] || contents["#{type.split('/')[0]}/*"] || contents['*/*'] || contents[nil]
11
+ semi = content_type.index(';')
12
+ type = semi ? content_type[0, semi] : content_type
13
+ slash = type.index('/') || type.length
14
+ contents[type] || contents["#{type[0, slash]}/*"] || contents['*/*'] || contents[nil]
13
15
  end
14
16
  end
15
17
  end
@@ -6,8 +6,6 @@ module OpenapiFirst
6
6
  class PathTemplate
7
7
  # See also https://spec.openapis.org/oas/v3.1.0#path-templating
8
8
  TEMPLATE_EXPRESSION = /(\{[^{}]+\})/
9
- TEMPLATE_EXPRESSION_NAME = /\{([^{}]+)\}/
10
- ALLOWED_PARAMETER_CHARACTERS = %r{([^/?#]+)}
11
9
 
12
10
  def self.template?(string)
13
11
  string.include?('{')
@@ -15,7 +13,6 @@ module OpenapiFirst
15
13
 
16
14
  def initialize(template)
17
15
  @template = template
18
- @names = template.scan(TEMPLATE_EXPRESSION_NAME).flatten
19
16
  @pattern = build_pattern(template)
20
17
  end
21
18
 
@@ -25,20 +22,22 @@ module OpenapiFirst
25
22
 
26
23
  def match(path)
27
24
  return {} if path == @template
28
- return if @names.empty?
29
25
 
30
26
  matches = path.match(@pattern)
31
27
  return unless matches
32
28
 
33
- values = matches.captures
34
- @names.zip(values).to_h
29
+ matches.named_captures
35
30
  end
36
31
 
37
32
  private
38
33
 
39
34
  def build_pattern(template)
40
35
  parts = template.split(TEMPLATE_EXPRESSION).map! do |part|
41
- part.start_with?('{') ? ALLOWED_PARAMETER_CHARACTERS : Regexp.escape(part)
36
+ if part.start_with?('{')
37
+ "(?<#{part[1..-2]}>[^/?#]+)"
38
+ else
39
+ Regexp.escape(part)
40
+ end
42
41
  end
43
42
 
44
43
  /^#{parts.join}$/
@@ -97,15 +97,15 @@ module OpenapiFirst
97
97
  found = @static[request_path]
98
98
  return [found, {}] if found
99
99
 
100
- matches = @dynamic.filter_map do |_path, path_item|
100
+ @dynamic.each_value.reduce(nil) do |best, path_item|
101
101
  params = path_item[:template].match(request_path)
102
- next unless params
102
+ next best unless params
103
103
 
104
- [path_item, params]
105
- end
106
- return matches.first if matches.length == 1
104
+ candidate = [path_item, params]
105
+ next candidate unless best
107
106
 
108
- matches&.min_by { |match| match[1].values.sum(&:length) }
107
+ params.values.sum(&:length) < best[1].values.sum(&:length) ? candidate : best
108
+ end
109
109
  end
110
110
  end
111
111
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '3.2.1'
4
+ VERSION = '3.3.0'
5
5
  end
data/lib/openapi_first.rb CHANGED
@@ -22,6 +22,11 @@ module OpenapiFirst
22
22
 
23
23
  FAILURE = :openapi_first_validation_failure
24
24
 
25
+ # Clears cached files
26
+ def self.clear_cache!
27
+ FileLoader.clear_cache!
28
+ end
29
+
25
30
  # @return [Configuration]
26
31
  def self.configuration
27
32
  @configuration ||= Configuration.new
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.2.1
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
@@ -186,7 +186,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
186
  - !ruby/object:Gem::Version
187
187
  version: '0'
188
188
  requirements: []
189
- rubygems_version: 3.6.7
189
+ rubygems_version: 4.0.6
190
190
  specification_version: 4
191
191
  summary: OpenAPI based request validation, response validation, contract-testing and
192
192
  coverage