ruby_routes 2.1.0 → 2.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 +4 -4
- data/README.md +232 -162
- data/lib/ruby_routes/constant.rb +137 -18
- data/lib/ruby_routes/lru_strategies/hit_strategy.rb +31 -4
- data/lib/ruby_routes/lru_strategies/miss_strategy.rb +21 -0
- data/lib/ruby_routes/node.rb +82 -41
- data/lib/ruby_routes/radix_tree/finder.rb +164 -0
- data/lib/ruby_routes/radix_tree/inserter.rb +98 -0
- data/lib/ruby_routes/radix_tree.rb +83 -142
- data/lib/ruby_routes/route/check_helpers.rb +109 -0
- data/lib/ruby_routes/route/constraint_validator.rb +159 -0
- data/lib/ruby_routes/route/param_support.rb +202 -0
- data/lib/ruby_routes/route/path_builder.rb +86 -0
- data/lib/ruby_routes/route/path_generation.rb +102 -0
- data/lib/ruby_routes/route/query_helpers.rb +56 -0
- data/lib/ruby_routes/route/segment_compiler.rb +163 -0
- data/lib/ruby_routes/route/small_lru.rb +96 -17
- data/lib/ruby_routes/route/validation_helpers.rb +151 -0
- data/lib/ruby_routes/route/warning_helpers.rb +54 -0
- data/lib/ruby_routes/route.rb +121 -451
- data/lib/ruby_routes/route_set/cache_helpers.rb +174 -0
- data/lib/ruby_routes/route_set/collection_helpers.rb +127 -0
- data/lib/ruby_routes/route_set.rb +126 -148
- data/lib/ruby_routes/router/build_helpers.rb +100 -0
- data/lib/ruby_routes/router/builder.rb +96 -0
- data/lib/ruby_routes/router/http_helpers.rb +135 -0
- data/lib/ruby_routes/router/resource_helpers.rb +137 -0
- data/lib/ruby_routes/router/scope_helpers.rb +109 -0
- data/lib/ruby_routes/router.rb +196 -179
- data/lib/ruby_routes/segment.rb +28 -8
- data/lib/ruby_routes/segments/base_segment.rb +40 -4
- data/lib/ruby_routes/segments/dynamic_segment.rb +48 -12
- data/lib/ruby_routes/segments/static_segment.rb +43 -7
- data/lib/ruby_routes/segments/wildcard_segment.rb +56 -12
- data/lib/ruby_routes/string_extensions.rb +52 -15
- data/lib/ruby_routes/url_helpers.rb +106 -24
- data/lib/ruby_routes/utility/inflector_utility.rb +35 -0
- data/lib/ruby_routes/utility/key_builder_utility.rb +179 -0
- data/lib/ruby_routes/utility/method_utility.rb +137 -0
- data/lib/ruby_routes/utility/path_utility.rb +89 -0
- data/lib/ruby_routes/utility/route_utility.rb +49 -0
- data/lib/ruby_routes/version.rb +3 -1
- data/lib/ruby_routes.rb +68 -11
- metadata +30 -7
@@ -0,0 +1,174 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyRoutes
|
4
|
+
class RouteSet
|
5
|
+
# CacheHelpers: extracted cache, request-key, and eviction logic to reduce
|
6
|
+
# the size of the main RouteSet class.
|
7
|
+
#
|
8
|
+
# This module provides methods for managing caches, request keys, and
|
9
|
+
# implementing eviction policies for route recognition.
|
10
|
+
module CacheHelpers
|
11
|
+
# Recognition cache statistics.
|
12
|
+
#
|
13
|
+
# @return [Hash] A hash containing:
|
14
|
+
# - `:hits` [Integer] The number of cache hits.
|
15
|
+
# - `:misses` [Integer] The number of cache misses.
|
16
|
+
# - `:hit_rate` [Float] The cache hit rate as a percentage.
|
17
|
+
# - `:size` [Integer] The current size of the recognition cache.
|
18
|
+
def cache_stats
|
19
|
+
total_requests = @cache_hits + @cache_misses
|
20
|
+
{
|
21
|
+
hits: @cache_hits,
|
22
|
+
misses: @cache_misses,
|
23
|
+
hit_rate: total_requests.zero? ? 0.0 : (@cache_hits.to_f / total_requests * 100.0),
|
24
|
+
size: @recognition_cache.size
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Set up caches and request-key ring.
|
31
|
+
#
|
32
|
+
# Initializes the internal data structures for managing routes, named routes,
|
33
|
+
# recognition cache, and request-key ring buffer.
|
34
|
+
#
|
35
|
+
# @return [void]
|
36
|
+
def setup_caches
|
37
|
+
@routes = []
|
38
|
+
@named_routes = {}
|
39
|
+
@recognition_cache = {}
|
40
|
+
@recognition_cache_max = 2048
|
41
|
+
@cache_hits = 0
|
42
|
+
@cache_misses = 0
|
43
|
+
@request_key_pool = {}
|
44
|
+
@request_key_ring = Array.new(RubyRoutes::Constant::REQUEST_KEY_CAPACITY)
|
45
|
+
@entry_count = 0
|
46
|
+
@ring_index = 0
|
47
|
+
end
|
48
|
+
|
49
|
+
# Fetch (or build) a composite request cache key with ring-buffer eviction.
|
50
|
+
#
|
51
|
+
# Ensures consistent use of frozen method/path keys to avoid mixed key space bugs.
|
52
|
+
#
|
53
|
+
# @param http_method [String, Symbol] The HTTP method (e.g., `:get`, `:post`).
|
54
|
+
# @param request_path [String] The request path.
|
55
|
+
# @return [String] The composite request key.
|
56
|
+
def fetch_request_key(http_method, request_path)
|
57
|
+
method_key, path_key = normalize_keys(http_method, request_path)
|
58
|
+
composite_key = build_composite_key(method_key, path_key)
|
59
|
+
|
60
|
+
return composite_key if handle_cache_hit(method_key, path_key, composite_key)
|
61
|
+
|
62
|
+
handle_cache_miss(method_key, path_key, composite_key)
|
63
|
+
composite_key
|
64
|
+
end
|
65
|
+
|
66
|
+
# Normalize keys.
|
67
|
+
#
|
68
|
+
# Converts the HTTP method and request path into frozen strings for consistent
|
69
|
+
# key usage.
|
70
|
+
#
|
71
|
+
# @param http_method [String, Symbol] The HTTP method.
|
72
|
+
# @param request_path [String] The request path.
|
73
|
+
# @return [Array<String>] An array containing the normalized method and path keys.
|
74
|
+
def normalize_keys(http_method, request_path)
|
75
|
+
method_key = http_method.is_a?(String) ? http_method.upcase.freeze : http_method.to_s.upcase.freeze
|
76
|
+
path_key = request_path.is_a?(String) ? request_path.freeze : request_path.to_s.freeze
|
77
|
+
[method_key, path_key]
|
78
|
+
end
|
79
|
+
|
80
|
+
# Build composite key.
|
81
|
+
#
|
82
|
+
# Combines the HTTP method and path into a single composite key.
|
83
|
+
#
|
84
|
+
# @param method_key [String] The normalized HTTP method key.
|
85
|
+
# @param path_key [String] The normalized path key.
|
86
|
+
# @return [String] The composite key.
|
87
|
+
def build_composite_key(method_key, path_key)
|
88
|
+
"#{method_key}:#{path_key}".freeze
|
89
|
+
end
|
90
|
+
|
91
|
+
# Handle cache hit.
|
92
|
+
#
|
93
|
+
# Checks if the composite key already exists in the request key pool.
|
94
|
+
#
|
95
|
+
# @param method_key [String] The normalized HTTP method key.
|
96
|
+
# @param path_key [String] The normalized path key.
|
97
|
+
# @param _composite_key [String] The composite key (unused).
|
98
|
+
# @return [Boolean] `true` if the key exists, `false` otherwise.
|
99
|
+
def handle_cache_hit(method_key, path_key, _composite_key)
|
100
|
+
return true if @request_key_pool[method_key]&.key?(path_key)
|
101
|
+
|
102
|
+
false
|
103
|
+
end
|
104
|
+
|
105
|
+
# Handle cache miss.
|
106
|
+
#
|
107
|
+
# Adds the composite key to the request key pool and manages the ring buffer
|
108
|
+
# for eviction.
|
109
|
+
#
|
110
|
+
# @param method_key [String] The normalized HTTP method key.
|
111
|
+
# @param path_key [String] The normalized path key.
|
112
|
+
# @param composite_key [String] The composite key.
|
113
|
+
# @return [void]
|
114
|
+
def handle_cache_miss(method_key, path_key, composite_key)
|
115
|
+
@request_key_pool[method_key][path_key] = composite_key if @request_key_pool[method_key]
|
116
|
+
@request_key_pool[method_key] = { path_key => composite_key } unless @request_key_pool[method_key]
|
117
|
+
|
118
|
+
if @entry_count < RubyRoutes::Constant::REQUEST_KEY_CAPACITY
|
119
|
+
@request_key_ring[@entry_count] = [method_key, path_key]
|
120
|
+
@entry_count += 1
|
121
|
+
else
|
122
|
+
evict_old_entry(method_key, path_key)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Evict old entry.
|
127
|
+
#
|
128
|
+
# Removes the oldest entry from the request key pool and updates the ring buffer.
|
129
|
+
#
|
130
|
+
# @param method_key [String] The normalized HTTP method key.
|
131
|
+
# @param path_key [String] The normalized path key.
|
132
|
+
# @return [void]
|
133
|
+
def evict_old_entry(method_key, path_key)
|
134
|
+
evict_method, evict_path = @request_key_ring[@ring_index]
|
135
|
+
if (evict_bucket = @request_key_pool[evict_method]) && evict_bucket.delete(evict_path) && evict_bucket.empty?
|
136
|
+
@request_key_pool.delete(evict_method)
|
137
|
+
end
|
138
|
+
@request_key_ring[@ring_index] = [method_key, path_key]
|
139
|
+
@ring_index += 1
|
140
|
+
@ring_index = 0 if @ring_index == RubyRoutes::Constant::REQUEST_KEY_CAPACITY
|
141
|
+
end
|
142
|
+
|
143
|
+
# Fetch cached recognition entry while updating hit counter.
|
144
|
+
#
|
145
|
+
# @param lookup_key [String] The cache lookup key.
|
146
|
+
# @return [Hash, nil] The cached recognition entry, or `nil` if not found.
|
147
|
+
def fetch_cached_recognition(lookup_key)
|
148
|
+
if (cached_result = @recognition_cache[lookup_key])
|
149
|
+
@cache_hits += 1
|
150
|
+
return cached_result
|
151
|
+
end
|
152
|
+
@cache_misses += 1
|
153
|
+
nil
|
154
|
+
end
|
155
|
+
|
156
|
+
# Cache insertion with simple segment eviction (25% oldest).
|
157
|
+
#
|
158
|
+
# Adds a new entry to the recognition cache, evicting the oldest 25% of entries
|
159
|
+
# if the cache exceeds its maximum size.
|
160
|
+
#
|
161
|
+
# @param cache_key [String] The cache key.
|
162
|
+
# @param entry [Hash] The cache entry.
|
163
|
+
# @return [void]
|
164
|
+
def insert_cache_entry(cache_key, entry)
|
165
|
+
if @recognition_cache.size >= @recognition_cache_max
|
166
|
+
@recognition_cache.keys.first(@recognition_cache_max / 4).each do |evict_key|
|
167
|
+
@recognition_cache.delete(evict_key)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
@recognition_cache[cache_key] = entry
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyRoutes
|
4
|
+
class RouteSet
|
5
|
+
# CollectionHelpers: extracted route collection and enumeration helpers
|
6
|
+
# to keep RouteSet implementation small.
|
7
|
+
#
|
8
|
+
# This module provides methods for managing and querying routes within a
|
9
|
+
# `RouteSet`. It includes functionality for adding, finding, clearing, and
|
10
|
+
# enumerating routes, as well as managing named routes and caches.
|
11
|
+
module CollectionHelpers
|
12
|
+
# Add a route object to internal structures.
|
13
|
+
#
|
14
|
+
# This method adds a route to the internal route collection, updates the
|
15
|
+
# radix tree for fast path/method lookups, and registers the route in the
|
16
|
+
# named routes collection if it has a name.
|
17
|
+
#
|
18
|
+
# @param route [Route] The route to add.
|
19
|
+
# @return [Route] The added route.
|
20
|
+
def add_to_collection(route)
|
21
|
+
@routes << route
|
22
|
+
@radix_tree.add(route.path, route.methods, route)
|
23
|
+
@named_routes[route.name] = route if route.named?
|
24
|
+
route
|
25
|
+
end
|
26
|
+
alias add_route add_to_collection
|
27
|
+
|
28
|
+
# Register a newly created Route (called from RouteUtility#define).
|
29
|
+
#
|
30
|
+
# This method initializes the route collection if it is not already set
|
31
|
+
# and adds the given route to the collection.
|
32
|
+
#
|
33
|
+
# @param route [Route] The route to register.
|
34
|
+
# @return [Route] The registered route.
|
35
|
+
def register(route)
|
36
|
+
(@routes ||= []) << route
|
37
|
+
route
|
38
|
+
end
|
39
|
+
|
40
|
+
# Find any route (no params) for a method/path.
|
41
|
+
#
|
42
|
+
# This method searches the radix tree for a route matching the given HTTP
|
43
|
+
# method and path.
|
44
|
+
#
|
45
|
+
# @param http_method [String, Symbol] The HTTP method (e.g., `:get`, `:post`).
|
46
|
+
# @param path [String] The path to match.
|
47
|
+
# @return [Route, nil] The matching route, or `nil` if no match is found.
|
48
|
+
def find_route(http_method, path)
|
49
|
+
route, _params = @radix_tree.find(path, http_method)
|
50
|
+
route
|
51
|
+
end
|
52
|
+
|
53
|
+
# Retrieve a named route.
|
54
|
+
#
|
55
|
+
# This method retrieves a route by its name from the named routes collection.
|
56
|
+
# If no route is found, it raises a `RouteNotFound` error.
|
57
|
+
#
|
58
|
+
# @param name [Symbol, String] The name of the route.
|
59
|
+
# @return [Route] The named route.
|
60
|
+
# @raise [RouteNotFound] If no route with the given name is found.
|
61
|
+
def find_named_route(name)
|
62
|
+
route = @named_routes[name]
|
63
|
+
raise RouteNotFound, "No route named '#{name}'" unless route
|
64
|
+
|
65
|
+
route
|
66
|
+
end
|
67
|
+
|
68
|
+
# Clear all routes and caches.
|
69
|
+
#
|
70
|
+
# This method clears the internal route collection, named routes, recognition
|
71
|
+
# cache, and radix tree. It also resets cache hit/miss counters and the
|
72
|
+
# request key pool.
|
73
|
+
#
|
74
|
+
# @return [void]
|
75
|
+
def clear!
|
76
|
+
@routes.clear
|
77
|
+
@named_routes.clear
|
78
|
+
@recognition_cache.clear
|
79
|
+
@cache_hits = 0
|
80
|
+
@cache_misses = 0
|
81
|
+
@radix_tree = RadixTree.new
|
82
|
+
@request_key_pool.clear
|
83
|
+
@request_key_ring.fill(nil)
|
84
|
+
@entry_count = 0
|
85
|
+
@ring_index = 0
|
86
|
+
end
|
87
|
+
|
88
|
+
# Get the number of routes.
|
89
|
+
#
|
90
|
+
# @return [Integer] The number of routes in the collection.
|
91
|
+
def size
|
92
|
+
@routes.size
|
93
|
+
end
|
94
|
+
|
95
|
+
# Check if the route collection is empty.
|
96
|
+
#
|
97
|
+
# @return [Boolean] `true` if the collection is empty, `false` otherwise.
|
98
|
+
def empty?
|
99
|
+
@routes.empty?
|
100
|
+
end
|
101
|
+
|
102
|
+
# Enumerate routes.
|
103
|
+
#
|
104
|
+
# This method yields each route in the collection to the given block. If no
|
105
|
+
# block is provided, it returns an enumerator.
|
106
|
+
#
|
107
|
+
# @yield [route] Yields each route in the collection.
|
108
|
+
# @return [Enumerator, self] An enumerator if no block is given, or `self`.
|
109
|
+
def each(&block)
|
110
|
+
return enum_for(:each) unless block
|
111
|
+
|
112
|
+
@routes.each(&block)
|
113
|
+
self
|
114
|
+
end
|
115
|
+
|
116
|
+
# Test membership.
|
117
|
+
#
|
118
|
+
# This method checks if the given route is included in the route collection.
|
119
|
+
#
|
120
|
+
# @param route [Route] The route to check.
|
121
|
+
# @return [Boolean] `true` if the route is in the collection, `false` otherwise.
|
122
|
+
def include?(route)
|
123
|
+
@routes.include?(route)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -1,192 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'utility/key_builder_utility'
|
4
|
+
require_relative 'utility/method_utility'
|
5
|
+
require_relative 'route_set/cache_helpers'
|
6
|
+
require_relative 'route_set/collection_helpers'
|
7
|
+
require_relative 'route/param_support'
|
8
|
+
|
1
9
|
module RubyRoutes
|
10
|
+
# RouteSet
|
11
|
+
#
|
12
|
+
# Collection + lookup facade for Route instances.
|
13
|
+
#
|
14
|
+
# Responsibilities:
|
15
|
+
# - Hold all defined routes (ordered).
|
16
|
+
# - Index named routes.
|
17
|
+
# - Provide fast recognition (method + path → route, params) with
|
18
|
+
# a small in‑memory recognition cache.
|
19
|
+
# - Delegate structural path matching to an internal RadixTree.
|
20
|
+
#
|
21
|
+
# Thread safety: not thread‑safe; build during boot, read per request.
|
22
|
+
#
|
23
|
+
# @api public (primary integration surface)
|
2
24
|
class RouteSet
|
3
25
|
attr_reader :routes
|
4
26
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
@recognition_cache = {}
|
11
|
-
@cache_hits = 0
|
12
|
-
@cache_misses = 0
|
13
|
-
@recognition_cache_max = 8192 # larger for better hit rates
|
14
|
-
end
|
15
|
-
|
16
|
-
def add_route(route)
|
17
|
-
@routes << route
|
18
|
-
@tree.add(route.path, route.methods, route)
|
19
|
-
@named_routes[route.name] = route if route.named?
|
20
|
-
# Clear recognition cache when routes change
|
21
|
-
@recognition_cache.clear if @recognition_cache.size > 100
|
22
|
-
route
|
23
|
-
end
|
27
|
+
include RubyRoutes::Utility::KeyBuilderUtility
|
28
|
+
include RubyRoutes::Utility::MethodUtility
|
29
|
+
include RubyRoutes::RouteSet::CacheHelpers
|
30
|
+
include RubyRoutes::Route::ParamSupport
|
31
|
+
include RubyRoutes::RouteSet::CollectionHelpers
|
24
32
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
def find_named_route(name)
|
33
|
-
route = @named_routes[name]
|
34
|
-
return route if route
|
35
|
-
raise RouteNotFound, "No route named '#{name}'"
|
33
|
+
# Initialize empty collection and caches.
|
34
|
+
#
|
35
|
+
# @return [void]
|
36
|
+
def initialize
|
37
|
+
setup_caches
|
38
|
+
setup_radix_tree
|
36
39
|
end
|
37
40
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
+
# Recognize a request (method + path) returning route + params.
|
42
|
+
#
|
43
|
+
# @param http_method [String, Symbol] The HTTP method (e.g., "GET").
|
44
|
+
# @param path [String] The request path.
|
45
|
+
# @return [Hash, nil] A hash containing the matched route and parameters, or `nil` if no match is found.
|
46
|
+
def match(http_method, path)
|
47
|
+
normalized_method = normalize_method_for_match(http_method)
|
48
|
+
raw_path = path.to_s
|
49
|
+
lookup_key = cache_key_for_request(normalized_method, raw_path)
|
41
50
|
|
42
|
-
|
43
|
-
cache_key = build_cache_key(method_up, request_path)
|
44
|
-
|
45
|
-
# Cache hit: return immediately (cached result includes full structure)
|
46
|
-
if (cached_result = @recognition_cache[cache_key])
|
47
|
-
@cache_hits += 1
|
51
|
+
if (cached_result = fetch_cached_recognition(lookup_key))
|
48
52
|
return cached_result
|
49
53
|
end
|
50
54
|
|
51
|
-
|
52
|
-
|
53
|
-
# Use thread-local params to avoid allocations
|
54
|
-
params = get_thread_local_params
|
55
|
-
handler, _ = @tree.find(request_path, method_up, params)
|
56
|
-
return nil unless handler
|
57
|
-
|
58
|
-
route = handler
|
59
|
-
|
60
|
-
# Fast path: merge defaults only if they exist
|
61
|
-
merge_defaults(route, params) if route.defaults && !route.defaults.empty?
|
62
|
-
|
63
|
-
# Fast path: parse query params only if needed
|
64
|
-
if request_path.include?('?')
|
65
|
-
merge_query_params(route, request_path, params)
|
66
|
-
end
|
67
|
-
|
68
|
-
# Create return hash and cache the complete result
|
69
|
-
result_params = params.dup
|
70
|
-
result = {
|
71
|
-
route: route,
|
72
|
-
params: result_params,
|
73
|
-
controller: route.controller,
|
74
|
-
action: route.action
|
75
|
-
}.freeze
|
76
|
-
|
77
|
-
insert_cache_entry(cache_key, result)
|
55
|
+
result = perform_match(normalized_method, raw_path)
|
56
|
+
insert_cache_entry(lookup_key, result) if result
|
78
57
|
result
|
79
58
|
end
|
80
59
|
|
81
|
-
|
60
|
+
# Convenience alias for Rack‑style recognizer.
|
61
|
+
#
|
62
|
+
# @param path [String] The request path.
|
63
|
+
# @param method [String, Symbol] The HTTP method (default: "GET").
|
64
|
+
# @return [Hash, nil] A hash containing the matched route and parameters, or `nil` if no match is found.
|
65
|
+
def recognize_path(path, method = 'GET')
|
82
66
|
match(method, path)
|
83
67
|
end
|
84
68
|
|
69
|
+
# Generate path via named route.
|
70
|
+
#
|
71
|
+
# @param name [Symbol, String] The name of the route.
|
72
|
+
# @param params [Hash] The parameters for path generation.
|
73
|
+
# @return [String] The generated path.
|
85
74
|
def generate_path(name, params = {})
|
86
|
-
route =
|
87
|
-
|
88
|
-
route.generate_path(params)
|
89
|
-
else
|
90
|
-
raise RouteNotFound, "No route named '#{name}'"
|
91
|
-
end
|
75
|
+
route = find_named_route(name)
|
76
|
+
route.generate_path(params)
|
92
77
|
end
|
93
78
|
|
79
|
+
# Generate path from a direct route reference.
|
80
|
+
#
|
81
|
+
# @param route [Route] The route instance.
|
82
|
+
# @param params [Hash] The parameters for path generation.
|
83
|
+
# @return [String] The generated path.
|
94
84
|
def generate_path_from_route(route, params = {})
|
95
85
|
route.generate_path(params)
|
96
86
|
end
|
97
87
|
|
98
|
-
|
99
|
-
@routes.clear
|
100
|
-
@named_routes.clear
|
101
|
-
@recognition_cache.clear
|
102
|
-
@tree = RadixTree.new
|
103
|
-
@cache_hits = @cache_misses = 0
|
104
|
-
end
|
88
|
+
private
|
105
89
|
|
106
|
-
|
107
|
-
|
90
|
+
# Set up the radix tree for structural path matching.
|
91
|
+
#
|
92
|
+
# @return [void]
|
93
|
+
def setup_radix_tree
|
94
|
+
@radix_tree = RadixTree.new
|
108
95
|
end
|
109
|
-
alias_method :length, :size
|
110
96
|
|
111
|
-
|
112
|
-
|
97
|
+
# Normalize the HTTP method for matching.
|
98
|
+
#
|
99
|
+
# @param http_method [String, Symbol] The HTTP method.
|
100
|
+
# @return [String] The normalized HTTP method.
|
101
|
+
def normalize_method_for_match(http_method)
|
102
|
+
if http_method.is_a?(String) && normalize_http_method(http_method).equal?(http_method)
|
103
|
+
http_method
|
104
|
+
else
|
105
|
+
normalize_http_method(http_method)
|
106
|
+
end
|
113
107
|
end
|
114
108
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
109
|
+
# Perform the route matching process.
|
110
|
+
#
|
111
|
+
# @param normalized_method [String] The normalized HTTP method.
|
112
|
+
# @param raw_path [String] The raw request path.
|
113
|
+
# @return [Hash, nil] A hash containing the matched route and parameters, or `nil` if no match is found.
|
114
|
+
def perform_match(normalized_method, raw_path)
|
115
|
+
path_without_query, _query = raw_path.split('?', 2)
|
116
|
+
matched_route, extracted_params = @radix_tree.find(path_without_query, normalized_method)
|
117
|
+
return nil unless matched_route
|
119
118
|
|
120
|
-
|
121
|
-
|
122
|
-
end
|
119
|
+
# Ensure we have a mutable hash for merging defaults / query params.
|
120
|
+
extracted_params = extracted_params.dup if extracted_params&.frozen?
|
123
121
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
hit_rate = total > 0 ? (@cache_hits.to_f / total * 100).round(2) : 0
|
128
|
-
{
|
129
|
-
hits: @cache_hits,
|
130
|
-
misses: @cache_misses,
|
131
|
-
hit_rate: "#{hit_rate}%",
|
132
|
-
size: @recognition_cache.size
|
133
|
-
}
|
122
|
+
merge_query_params(matched_route, raw_path, extracted_params)
|
123
|
+
merge_defaults(matched_route, extracted_params)
|
124
|
+
build_match_result(matched_route, extracted_params)
|
134
125
|
end
|
135
126
|
|
136
|
-
|
127
|
+
# Merge default parameters into the extracted parameters.
|
128
|
+
#
|
129
|
+
# @param matched_route [Route] The matched route.
|
130
|
+
# @param extracted_params [Hash] The extracted parameters.
|
131
|
+
# @return [void]
|
132
|
+
def merge_defaults(matched_route, extracted_params)
|
133
|
+
return unless matched_route.respond_to?(:defaults) && matched_route.defaults
|
137
134
|
|
138
|
-
|
139
|
-
def method_lookup(method)
|
140
|
-
@method_cache ||= Hash.new { |h, k| h[k] = k.to_s.upcase.freeze }
|
141
|
-
@method_cache[method]
|
135
|
+
matched_route.defaults.each { |key, value| extracted_params[key] = value unless extracted_params.key?(key) }
|
142
136
|
end
|
143
137
|
|
144
|
-
#
|
145
|
-
|
146
|
-
|
147
|
-
|
138
|
+
# Build the match result hash.
|
139
|
+
#
|
140
|
+
# @param matched_route [Route] The matched route.
|
141
|
+
# @param extracted_params [Hash] The extracted parameters.
|
142
|
+
# @return [Hash] A hash containing the matched route, parameters, controller, and action.
|
143
|
+
def build_match_result(matched_route, extracted_params)
|
144
|
+
{
|
145
|
+
route: matched_route,
|
146
|
+
params: extracted_params,
|
147
|
+
controller: matched_route.controller,
|
148
|
+
action: matched_route.action
|
149
|
+
}
|
148
150
|
end
|
149
151
|
|
150
|
-
#
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
152
|
+
# Obtain a pooled hash for temporary parameters.
|
153
|
+
#
|
154
|
+
# @return [Hash] A thread-local hash for temporary parameter storage.
|
155
|
+
def thread_local_params
|
156
|
+
thread_params = Thread.current[:ruby_routes_params_pool] ||= []
|
157
|
+
thread_params.empty? ? {} : thread_params.pop.clear
|
156
158
|
end
|
157
159
|
|
160
|
+
# Return a parameters hash to the thread-local pool.
|
161
|
+
#
|
162
|
+
# @param params [Hash] The parameters hash to return.
|
163
|
+
# @return [void]
|
158
164
|
def return_params_to_pool(params)
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
# Fast defaults merging
|
163
|
-
def merge_defaults(route, params)
|
164
|
-
route.defaults.each do |key, value|
|
165
|
-
params[key] = value unless params.key?(key)
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
# Fast query params merging
|
170
|
-
def merge_query_params(route, request_path, params)
|
171
|
-
if route.respond_to?(:parse_query_params)
|
172
|
-
qp = route.parse_query_params(request_path)
|
173
|
-
params.merge!(qp) unless qp.empty?
|
174
|
-
elsif route.respond_to?(:query_params)
|
175
|
-
qp = route.query_params(request_path)
|
176
|
-
params.merge!(qp) unless qp.empty?
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
# Efficient cache insertion with LRU eviction
|
181
|
-
def insert_cache_entry(cache_key, cache_entry)
|
182
|
-
@recognition_cache[cache_key] = cache_entry
|
183
|
-
|
184
|
-
# Simple eviction: clear cache when it gets too large
|
185
|
-
if @recognition_cache.size > @recognition_cache_max
|
186
|
-
# Keep most recently used half
|
187
|
-
keys_to_delete = @recognition_cache.keys[0...(@recognition_cache_max / 2)]
|
188
|
-
keys_to_delete.each { |k| @recognition_cache.delete(k) }
|
189
|
-
end
|
165
|
+
params.clear
|
166
|
+
thread_pool = Thread.current[:ruby_routes_params_pool] ||= []
|
167
|
+
thread_pool << params if thread_pool.size < 10 # Limit pool size
|
190
168
|
end
|
191
169
|
end
|
192
170
|
end
|