Skip to content

Commit b6de667

Browse files
authored
Merge pull request #462 from moberegger/moberegger/optimize-find_path_item
Optimize OpenAPI routing
2 parents daa92a6 + c1ba08b commit b6de667

4 files changed

Lines changed: 18 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
## Unreleased
44

55
- 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!`.
6-
6+
- Optimized `OpenapiFirst::Router#match` for faster path matching and reduced memory allocation.
7+
-
78
## 3.2.1
89

910
- Don't raise `UnknownQueryParameterError` if request is ignored in tests. Fixes [#441](https://github.com/ahx/openapi_first/issues/441).

lib/openapi_first/router.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,15 @@ def find_path_item(request_path)
9797
found = @static[request_path]
9898
return [found, {}] if found
9999

100-
matches = @dynamic.filter_map do |_path, path_item|
100+
@dynamic.each_value.reduce(nil) do |best, path_item|
101101
params = path_item[:template].match(request_path)
102-
next unless params
102+
next best unless params
103103

104-
[path_item, params]
105-
end
106-
return matches.first if matches.length == 1
104+
candidate = [path_item, params]
105+
next candidate unless best
107106

108-
matches&.min_by { |match| match[1].values.sum(&:length) }
107+
params.values.sum(&:length) < best[1].values.sum(&:length) ? candidate : best
108+
end
109109
end
110110
end
111111
end

lib/openapi_first/router/find_content.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ def self.call(contents, content_type)
88
return contents[nil] if content_type.nil? || content_type.empty?
99

1010
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]
1315
end
1416
end
1517
end

lib/openapi_first/router/path_template.rb

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,13 @@ class Router
66
class PathTemplate
77
# See also https://spec.openapis.org/oas/v3.1.0#path-templating
88
TEMPLATE_EXPRESSION = /(\{[^{}]+\})/
9-
TEMPLATE_EXPRESSION_NAME = /\{([^{}]+)\}/
10-
ALLOWED_PARAMETER_CHARACTERS = %r{([^/?#]+)}
119

1210
def self.template?(string)
1311
string.include?('{')
1412
end
1513

1614
def initialize(template)
1715
@template = template
18-
@names = template.scan(TEMPLATE_EXPRESSION_NAME).flatten
1916
@pattern = build_pattern(template)
2017
end
2118

@@ -25,20 +22,22 @@ def to_s
2522

2623
def match(path)
2724
return {} if path == @template
28-
return if @names.empty?
2925

3026
matches = path.match(@pattern)
3127
return unless matches
3228

33-
values = matches.captures
34-
@names.zip(values).to_h
29+
matches.named_captures
3530
end
3631

3732
private
3833

3934
def build_pattern(template)
4035
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
4241
end
4342

4443
/^#{parts.join}$/

0 commit comments

Comments
 (0)