diff --git a/CHANGELOG.md b/CHANGELOG.md index 822629a4..2f47754d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ ## Unreleased - 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!`. - +- Optimized `OpenapiFirst::Router#match` for faster path matching and reduced memory allocation. +- ## 3.2.1 - Don't raise `UnknownQueryParameterError` if request is ignored in tests. Fixes [#441](https://github.com/ahx/openapi_first/issues/441). diff --git a/lib/openapi_first/router.rb b/lib/openapi_first/router.rb index 5f7189fc..d31de3e1 100644 --- a/lib/openapi_first/router.rb +++ b/lib/openapi_first/router.rb @@ -97,15 +97,15 @@ def find_path_item(request_path) found = @static[request_path] return [found, {}] if found - matches = @dynamic.filter_map do |_path, path_item| + @dynamic.each_value.reduce(nil) do |best, path_item| params = path_item[:template].match(request_path) - next unless params + next best unless params - [path_item, params] - end - return matches.first if matches.length == 1 + candidate = [path_item, params] + next candidate unless best - matches&.min_by { |match| match[1].values.sum(&:length) } + params.values.sum(&:length) < best[1].values.sum(&:length) ? candidate : best + end end end end diff --git a/lib/openapi_first/router/find_content.rb b/lib/openapi_first/router/find_content.rb index 04ab42fb..0349dfa9 100644 --- a/lib/openapi_first/router/find_content.rb +++ b/lib/openapi_first/router/find_content.rb @@ -8,8 +8,10 @@ def self.call(contents, content_type) return contents[nil] if content_type.nil? || content_type.empty? contents.fetch(content_type) do - type = content_type.split(';')[0] - contents[type] || contents["#{type.split('/')[0]}/*"] || contents['*/*'] || contents[nil] + semi = content_type.index(';') + type = semi ? content_type[0, semi] : content_type + slash = type.index('/') || type.length + contents[type] || contents["#{type[0, slash]}/*"] || contents['*/*'] || contents[nil] end end end diff --git a/lib/openapi_first/router/path_template.rb b/lib/openapi_first/router/path_template.rb index 2a730853..783b3057 100644 --- a/lib/openapi_first/router/path_template.rb +++ b/lib/openapi_first/router/path_template.rb @@ -6,8 +6,6 @@ class Router class PathTemplate # See also https://spec.openapis.org/oas/v3.1.0#path-templating TEMPLATE_EXPRESSION = /(\{[^{}]+\})/ - TEMPLATE_EXPRESSION_NAME = /\{([^{}]+)\}/ - ALLOWED_PARAMETER_CHARACTERS = %r{([^/?#]+)} def self.template?(string) string.include?('{') @@ -15,7 +13,6 @@ def self.template?(string) def initialize(template) @template = template - @names = template.scan(TEMPLATE_EXPRESSION_NAME).flatten @pattern = build_pattern(template) end @@ -25,20 +22,22 @@ def to_s def match(path) return {} if path == @template - return if @names.empty? matches = path.match(@pattern) return unless matches - values = matches.captures - @names.zip(values).to_h + matches.named_captures end private def build_pattern(template) parts = template.split(TEMPLATE_EXPRESSION).map! do |part| - part.start_with?('{') ? ALLOWED_PARAMETER_CHARACTERS : Regexp.escape(part) + if part.start_with?('{') + "(?<#{part[1..-2]}>[^/?#]+)" + else + Regexp.escape(part) + end end /^#{parts.join}$/