ruby_routes 2.4.0 → 2.6.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 +0 -23
- data/lib/ruby_routes/constant.rb +20 -3
- data/lib/ruby_routes/lru_strategies/hit_strategy.rb +1 -1
- data/lib/ruby_routes/node.rb +15 -87
- data/lib/ruby_routes/radix_tree/finder.rb +79 -52
- data/lib/ruby_routes/radix_tree/inserter.rb +2 -54
- data/lib/ruby_routes/radix_tree/traversal_strategy/base.rb +18 -0
- data/lib/ruby_routes/radix_tree/traversal_strategy/generic_loop.rb +25 -0
- data/lib/ruby_routes/radix_tree/traversal_strategy/unrolled.rb +45 -0
- data/lib/ruby_routes/radix_tree/traversal_strategy.rb +26 -0
- data/lib/ruby_routes/radix_tree.rb +12 -62
- data/lib/ruby_routes/route/check_helpers.rb +3 -3
- data/lib/ruby_routes/route/constraint_validator.rb +24 -1
- data/lib/ruby_routes/route/matcher.rb +11 -0
- data/lib/ruby_routes/route/param_support.rb +9 -7
- data/lib/ruby_routes/route/path_builder.rb +11 -6
- data/lib/ruby_routes/route/path_generation.rb +5 -1
- data/lib/ruby_routes/route/small_lru.rb +43 -2
- data/lib/ruby_routes/route/validation_helpers.rb +6 -36
- data/lib/ruby_routes/route/warning_helpers.rb +7 -5
- data/lib/ruby_routes/route.rb +35 -55
- data/lib/ruby_routes/route_set/cache_helpers.rb +32 -13
- data/lib/ruby_routes/route_set/collection_helpers.rb +15 -14
- data/lib/ruby_routes/route_set.rb +32 -69
- data/lib/ruby_routes/router/build_helpers.rb +1 -1
- data/lib/ruby_routes/router/builder.rb +16 -33
- data/lib/ruby_routes/router/http_helpers.rb +13 -49
- data/lib/ruby_routes/router/resource_helpers.rb +26 -39
- data/lib/ruby_routes/router/scope_helpers.rb +26 -14
- data/lib/ruby_routes/router.rb +41 -24
- data/lib/ruby_routes/segment.rb +3 -3
- data/lib/ruby_routes/segments/base_segment.rb +8 -0
- data/lib/ruby_routes/segments/static_segment.rb +3 -1
- data/lib/ruby_routes/strategies/base.rb +18 -0
- data/lib/ruby_routes/strategies/hash_based_strategy.rb +33 -0
- data/lib/ruby_routes/strategies/hybrid_strategy.rb +70 -0
- data/lib/ruby_routes/strategies/radix_tree_strategy.rb +24 -0
- data/lib/ruby_routes/strategies.rb +5 -0
- data/lib/ruby_routes/utility/key_builder_utility.rb +4 -26
- data/lib/ruby_routes/utility/method_utility.rb +18 -17
- data/lib/ruby_routes/utility/path_utility.rb +18 -7
- data/lib/ruby_routes/version.rb +1 -1
- data/lib/ruby_routes.rb +3 -1
- metadata +11 -2
- data/lib/ruby_routes/string_extensions.rb +0 -65
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 33b60562a702b7d2def6960af7e255d460b17e508367563aefd9ef85856abe52
|
4
|
+
data.tar.gz: be3560f98947dd313afeef38603a9f97bb6a39917d471db9a26b4dcdbb42d2bc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 87b6d61fda5213a0db9e5fba0ad35a3158341e3df9af8e708c38aee60524c3b1c4d2ca85051a4578c3caa57dcfc5fc01d3c37c833652fcb69bd23b6c35580a2e
|
7
|
+
data.tar.gz: 2378650e65113fc8d29c2062dbfb85127629f18d7f82a4dc00a98f5453b9c933c3fce20cbcc27e6a25b9d07313fec4c03778015ab0a55cf2e55abd4fb251029b
|
data/README.md
CHANGED
@@ -447,29 +447,6 @@ This gem is available as open source under the terms of the [MIT License](LICENS
|
|
447
447
|
|
448
448
|
This gem was inspired by Rails routing and aims to provide a lightweight alternative for Ruby applications that need flexible routing without the full Rails framework.
|
449
449
|
|
450
|
-
## Thread-safe Build (Isolated Builder)
|
451
|
-
|
452
|
-
Use the builder to accumulate routes without mutating a live router:
|
453
|
-
|
454
|
-
```ruby
|
455
|
-
router = RubyRoutes::Router.build do
|
456
|
-
resources :users
|
457
|
-
namespace :admin do
|
458
|
-
resources :posts
|
459
|
-
end
|
460
|
-
end
|
461
|
-
# router is now finalized (immutable)
|
462
|
-
```
|
463
|
-
|
464
|
-
If you need manual steps:
|
465
|
-
|
466
|
-
```ruby
|
467
|
-
builder = RubyRoutes::Router::Builder.new do
|
468
|
-
get '/health', to: 'system#health'
|
469
|
-
end
|
470
|
-
router = builder.build # finalized
|
471
|
-
```
|
472
|
-
|
473
450
|
## Fluent Method Chaining
|
474
451
|
|
475
452
|
For a more concise style, the routing DSL supports method chaining:
|
data/lib/ruby_routes/constant.rb
CHANGED
@@ -74,6 +74,23 @@ module RubyRoutes
|
|
74
74
|
default: ->(_node, _segment, _idx, _segments, _params) { nil }
|
75
75
|
}.freeze
|
76
76
|
|
77
|
+
TRAVERSAL_STRATEGIES = {
|
78
|
+
static: lambda do |node, segment, _index, _segments|
|
79
|
+
static_child = node.static_children[segment]
|
80
|
+
[static_child, false, Constant::EMPTY_HASH] if static_child
|
81
|
+
end,
|
82
|
+
dynamic: lambda do |node, segment, _index, _segments|
|
83
|
+
dynamic_child = node.dynamic_child
|
84
|
+
[dynamic_child, false, { dynamic_child.param_name => segment }] if dynamic_child
|
85
|
+
end,
|
86
|
+
wildcard: lambda do |node, _segment, index, segments|
|
87
|
+
wildcard_child = node.wildcard_child
|
88
|
+
[wildcard_child, true, { wildcard_child.param_name => segments[index..-1].join('/') }] if wildcard_child
|
89
|
+
end
|
90
|
+
}.freeze
|
91
|
+
|
92
|
+
TRAVERSAL_ORDER = %i[static dynamic wildcard].freeze
|
93
|
+
|
77
94
|
# Singleton instances to avoid per-cache strategy allocations.
|
78
95
|
#
|
79
96
|
# @return [RubyRoutes::LruStrategies::HitStrategy, RubyRoutes::LruStrategies::MissStrategy]
|
@@ -105,7 +122,7 @@ module RubyRoutes
|
|
105
122
|
# recently used entries will be evicted.
|
106
123
|
#
|
107
124
|
# @return [Integer]
|
108
|
-
|
125
|
+
CACHE_SIZE = 2048
|
109
126
|
|
110
127
|
# HTTP method constants.
|
111
128
|
HTTP_GET = 'GET'
|
@@ -146,7 +163,7 @@ module RubyRoutes
|
|
146
163
|
# Default result for no traversal match.
|
147
164
|
#
|
148
165
|
# @return [Array]
|
149
|
-
NO_TRAVERSAL_RESULT = [nil, false,
|
166
|
+
NO_TRAVERSAL_RESULT = [nil, false, EMPTY_HASH].freeze
|
150
167
|
|
151
168
|
# Built-in validators for constraints.
|
152
169
|
#
|
@@ -171,7 +188,7 @@ module RubyRoutes
|
|
171
188
|
segment_string = raw.to_s
|
172
189
|
dispatch_key = segment_string.empty? ? :default : segment_string.getbyte(0)
|
173
190
|
factory = DESCRIPTOR_FACTORIES[dispatch_key] || DESCRIPTOR_FACTORIES[:default]
|
174
|
-
factory.call(segment_string)
|
191
|
+
factory.call(segment_string).freeze
|
175
192
|
end
|
176
193
|
end
|
177
194
|
end
|
@@ -30,7 +30,7 @@ module RubyRoutes
|
|
30
30
|
lru.increment_hits
|
31
31
|
# Internal storage name (@hash) is intentionally accessed reflectively
|
32
32
|
# to keep strategy decoupled from public API surface.
|
33
|
-
store = lru.
|
33
|
+
store = lru.hash
|
34
34
|
value = store.delete(key)
|
35
35
|
store[key] = value
|
36
36
|
value
|
data/lib/ruby_routes/node.rb
CHANGED
@@ -1,114 +1,42 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'segment'
|
4
|
-
require_relative 'utility/path_utility'
|
5
4
|
require_relative 'utility/method_utility'
|
5
|
+
require_relative 'constant'
|
6
6
|
|
7
7
|
module RubyRoutes
|
8
|
-
# Node
|
9
|
-
#
|
10
|
-
# A single vertex in the routing radix tree.
|
11
|
-
#
|
12
|
-
# Structure:
|
13
|
-
# - static_children: Hash<String, Node> exact literal matches.
|
14
|
-
# - dynamic_child: Node (":param") matches any single segment, captures value.
|
15
|
-
# - wildcard_child: Node ("*splat") matches remaining segments (greedy).
|
16
|
-
#
|
17
|
-
# Handlers:
|
18
|
-
# - @handlers maps canonical HTTP method strings (e.g. "GET") to Route objects (or callable handlers).
|
19
|
-
# - @is_endpoint marks that at least one handler is attached (terminal path).
|
20
|
-
#
|
21
|
-
# Matching precedence (most → least specific):
|
22
|
-
# static → dynamic → wildcard
|
23
|
-
#
|
24
|
-
# Thread safety: not thread-safe (build during boot).
|
25
|
-
#
|
26
|
-
# @api internal
|
27
8
|
class Node
|
9
|
+
include RubyRoutes::Utility::MethodUtility
|
10
|
+
|
28
11
|
attr_accessor :param_name, :is_endpoint, :dynamic_child, :wildcard_child
|
29
12
|
attr_reader :handlers, :static_children
|
30
13
|
|
31
|
-
include RubyRoutes::Utility::PathUtility
|
32
|
-
include RubyRoutes::Utility::MethodUtility
|
33
|
-
|
34
14
|
def initialize
|
35
|
-
@is_endpoint
|
36
|
-
@handlers
|
37
|
-
@static_children
|
38
|
-
@dynamic_child
|
39
|
-
@wildcard_child
|
40
|
-
@param_name
|
15
|
+
@is_endpoint = false
|
16
|
+
@handlers = {}
|
17
|
+
@static_children = {}
|
18
|
+
@dynamic_child = nil
|
19
|
+
@wildcard_child = nil
|
20
|
+
@param_name = nil
|
41
21
|
end
|
42
22
|
|
43
|
-
# Register a handler under an HTTP method.
|
44
|
-
#
|
45
|
-
# @param method [String, Symbol]
|
46
|
-
# @param handler [Object] route or callable
|
47
|
-
# @return [Object] handler
|
48
23
|
def add_handler(method, handler)
|
49
|
-
|
50
|
-
@handlers[
|
51
|
-
@is_endpoint
|
52
|
-
handler
|
24
|
+
method_str = normalize_http_method(method)
|
25
|
+
@handlers[method_str] = handler
|
26
|
+
@is_endpoint = true
|
53
27
|
end
|
54
28
|
|
55
|
-
# Fetch a handler for a method.
|
56
|
-
#
|
57
|
-
# @param method [String, Symbol]
|
58
|
-
# @return [Object, nil]
|
59
29
|
def get_handler(method)
|
60
30
|
@handlers[normalize_http_method(method)]
|
61
31
|
end
|
62
32
|
|
63
|
-
# Traverses from this node using a single path segment.
|
64
|
-
# Returns [next_node_or_nil, stop_traversal(Boolean), captured_params(Hash)].
|
65
|
-
#
|
66
|
-
# Optimized + simplified (cyclomatic / perceived complexity, length).
|
67
|
-
#
|
68
|
-
# @param segment [String] the path segment to match
|
69
|
-
# @param index [Integer] the segment index in the full path
|
70
|
-
# @param segments [Array<String>] all path segments
|
71
|
-
# @param params [Hash] (unused in this method; mutation deferred)
|
72
|
-
# @return [Array] [next_node, stop, captured] or NO_TRAVERSAL_RESULT
|
73
33
|
def traverse_for(segment, index, segments, params)
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
captured = capture_dynamic_param(@dynamic_child, segment)
|
78
|
-
return [@dynamic_child, false, captured]
|
79
|
-
end
|
80
|
-
|
81
|
-
if @wildcard_child
|
82
|
-
captured = capture_wildcard_param(@wildcard_child, segments, index)
|
83
|
-
return [@wildcard_child, true, captured]
|
34
|
+
RubyRoutes::Constant::TRAVERSAL_ORDER.each do |strategy_name|
|
35
|
+
result = RubyRoutes::Constant::TRAVERSAL_STRATEGIES[strategy_name].call(self, segment, index, segments)
|
36
|
+
return result if result
|
84
37
|
end
|
85
38
|
|
86
39
|
RubyRoutes::Constant::NO_TRAVERSAL_RESULT
|
87
40
|
end
|
88
|
-
|
89
|
-
private
|
90
|
-
|
91
|
-
# Captures a dynamic parameter value and returns it for later assignment.
|
92
|
-
#
|
93
|
-
# @param dynamic_node [Node] the dynamic child node
|
94
|
-
# @param value [String] the segment value to capture
|
95
|
-
# @return [Hash] captured parameter hash or empty hash if no param
|
96
|
-
def capture_dynamic_param(dynamic_node, value)
|
97
|
-
return {} unless dynamic_node.param_name
|
98
|
-
|
99
|
-
{ dynamic_node.param_name => value }
|
100
|
-
end
|
101
|
-
|
102
|
-
# Captures a wildcard parameter value and returns it for later assignment.
|
103
|
-
#
|
104
|
-
# @param wc_node [Node] the wildcard child node
|
105
|
-
# @param segments [Array<String>] the full path segments
|
106
|
-
# @param index [Integer] the current segment index
|
107
|
-
# @return [Hash] captured parameter hash or empty hash if no param
|
108
|
-
def capture_wildcard_param(wildcard_node, segments, index)
|
109
|
-
return {} unless wildcard_node.param_name
|
110
|
-
|
111
|
-
{ wildcard_node.param_name => segments[index..].join('/') }
|
112
|
-
end
|
113
41
|
end
|
114
42
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative '../constant'
|
4
|
+
require_relative 'traversal_strategy'
|
4
5
|
|
5
6
|
module RubyRoutes
|
6
7
|
class RadixTree
|
@@ -8,6 +9,21 @@ module RubyRoutes
|
|
8
9
|
# Handles path normalization, segment traversal, and parameter extraction.
|
9
10
|
module Finder
|
10
11
|
|
12
|
+
# Pre-allocated buffers to minimize object creation
|
13
|
+
EMPTY_PARAMS = {}.freeze
|
14
|
+
TRAVERSAL_STATE_TEMPLATE = {
|
15
|
+
current: nil,
|
16
|
+
best_node: nil,
|
17
|
+
best_params: nil,
|
18
|
+
best_captured: nil,
|
19
|
+
matched: false
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
# Reusable hash for captured parameters to avoid repeated allocations
|
23
|
+
PARAMS_BUFFER_KEY = :ruby_routes_finder_params_buffer
|
24
|
+
CAPTURED_PARAMS_BUFFER_KEY = :ruby_routes_finder_captured_params_buffer
|
25
|
+
STATE_BUFFER_KEY = :ruby_routes_finder_state_buffer
|
26
|
+
|
11
27
|
# Evaluate constraint rules for a candidate route.
|
12
28
|
#
|
13
29
|
# @param route_handler [Object]
|
@@ -18,32 +34,33 @@ module RubyRoutes
|
|
18
34
|
|
19
35
|
begin
|
20
36
|
# Use a duplicate to avoid unintended mutation by validators.
|
21
|
-
route_handler.validate_constraints_fast!(captured_params)
|
37
|
+
route_handler.validate_constraints_fast!(captured_params.dup)
|
22
38
|
true
|
23
39
|
rescue RubyRoutes::ConstraintViolation
|
24
40
|
false
|
25
41
|
end
|
26
42
|
end
|
27
43
|
|
28
|
-
private
|
29
|
-
|
30
44
|
# Finds a route handler for the given path and HTTP method.
|
31
45
|
#
|
32
46
|
# @param path_input [String] the input path to match
|
33
47
|
# @param method_input [String, Symbol] the HTTP method
|
34
48
|
# @param params_out [Hash] optional output hash for captured parameters
|
35
|
-
# @return [Array] [handler, params] or [nil,
|
36
|
-
def find(path_input,
|
37
|
-
path
|
38
|
-
|
39
|
-
|
49
|
+
# @return [Array] [handler, params] or [nil, {}] if no match
|
50
|
+
def find(path_input, http_method, params_out = nil)
|
51
|
+
# Handle nil or empty path input
|
52
|
+
return [nil, EMPTY_PARAMS] if path_input.nil?
|
53
|
+
|
54
|
+
method = normalize_http_method(http_method)
|
55
|
+
return root_match(method, params_out || EMPTY_PARAMS) if path_input.empty? || path_input == RubyRoutes::Constant::ROOT_PATH
|
40
56
|
|
41
|
-
segments = split_path_cached(
|
42
|
-
return [nil,
|
57
|
+
segments = split_path_cached(path_input)
|
58
|
+
return [nil, EMPTY_PARAMS] if segments.empty?
|
43
59
|
|
44
|
-
|
45
|
-
|
46
|
-
|
60
|
+
# Use thread-local, reusable hashes to avoid allocations
|
61
|
+
params = acquire_params_buffer(params_out)
|
62
|
+
state = acquire_state_buffer
|
63
|
+
captured_params = acquire_captured_params_buffer
|
47
64
|
|
48
65
|
result = perform_traversal(segments, state, method, params, captured_params)
|
49
66
|
return result unless result.nil?
|
@@ -54,17 +71,28 @@ module RubyRoutes
|
|
54
71
|
# Initializes the traversal state for route matching.
|
55
72
|
#
|
56
73
|
# @return [Hash] state hash with :current, :best_node, :best_params, :best_captured, :matched
|
57
|
-
def
|
58
|
-
{
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
74
|
+
def acquire_state_buffer
|
75
|
+
state = Thread.current[STATE_BUFFER_KEY] ||= {}
|
76
|
+
state.clear
|
77
|
+
state[:current] = @root
|
78
|
+
state
|
79
|
+
end
|
80
|
+
|
81
|
+
def acquire_params_buffer(initial_params)
|
82
|
+
buffer = Thread.current[PARAMS_BUFFER_KEY] ||= {}
|
83
|
+
buffer.clear
|
84
|
+
buffer.merge!(initial_params) if initial_params
|
85
|
+
buffer
|
86
|
+
end
|
87
|
+
|
88
|
+
def acquire_captured_params_buffer
|
89
|
+
buffer = Thread.current[CAPTURED_PARAMS_BUFFER_KEY] ||= {}
|
90
|
+
buffer.clear
|
91
|
+
buffer
|
65
92
|
end
|
66
93
|
|
67
94
|
# Performs traversal through path segments to find a matching route.
|
95
|
+
# Optimized for common cases of 1-3 segments.
|
68
96
|
#
|
69
97
|
# @param segments [Array<String>] path segments
|
70
98
|
# @param state [Hash] traversal state
|
@@ -72,17 +100,8 @@ module RubyRoutes
|
|
72
100
|
# @param params [Hash] parameters hash
|
73
101
|
# @param captured_params [Hash] hash to collect captured parameters
|
74
102
|
# @return [nil, Array] nil if traversal succeeds, Array from finalize_on_fail if traversal fails
|
75
|
-
def perform_traversal(segments, state, method, params, captured_params)
|
76
|
-
segments.
|
77
|
-
next_node, stop = traverse_for_segment(state[:current], segment, index, segments, params, captured_params)
|
78
|
-
return finalize_on_fail(state, method, params, captured_params) unless next_node
|
79
|
-
|
80
|
-
state[:current] = next_node
|
81
|
-
state[:matched] = true # Set matched to true if at least one segment matched
|
82
|
-
record_candidate(state, method, params, captured_params) if endpoint_with_method?(state[:current], method)
|
83
|
-
break if stop
|
84
|
-
end
|
85
|
-
nil # Return nil to indicate successful traversal
|
103
|
+
def perform_traversal(segments, state, method, params, captured_params) # rubocop:disable Metrics/AbcSize
|
104
|
+
TraversalStrategy.for(segments.size, self).execute(segments, state, method, params, captured_params)
|
86
105
|
end
|
87
106
|
|
88
107
|
# Traverses to the next node for a given segment.
|
@@ -93,10 +112,10 @@ module RubyRoutes
|
|
93
112
|
# @param segments [Array<String>] all segments
|
94
113
|
# @param params [Hash] parameters hash
|
95
114
|
# @param captured_params [Hash] hash to collect captured parameters
|
96
|
-
# @return [Array] [next_node, stop_traversal
|
115
|
+
# @return [Array] [next_node, stop_traversal]
|
97
116
|
def traverse_for_segment(node, segment, index, segments, params, captured_params)
|
98
117
|
next_node, stop, segment_captured = node.traverse_for(segment, index, segments, params)
|
99
|
-
|
118
|
+
merge_captured_params(params, captured_params, segment_captured)
|
100
119
|
[next_node, stop]
|
101
120
|
end
|
102
121
|
|
@@ -108,7 +127,7 @@ module RubyRoutes
|
|
108
127
|
# @param captured_params [Hash] captured parameters from traversal
|
109
128
|
def record_candidate(state, _method, params, captured_params)
|
110
129
|
state[:best_node] = state[:current]
|
111
|
-
state[:best_params] = params
|
130
|
+
state[:best_params] = params
|
112
131
|
state[:best_captured] = captured_params.dup
|
113
132
|
end
|
114
133
|
|
@@ -142,17 +161,17 @@ module RubyRoutes
|
|
142
161
|
# @param captured_params [Hash] captured parameters from traversal
|
143
162
|
# @return [Array] [handler, params] or [nil, params]
|
144
163
|
def finalize_success(state, method, params, captured_params)
|
145
|
-
|
146
|
-
return
|
164
|
+
handler, final_params = finalize_match(state[:current], method, params, captured_params)
|
165
|
+
return [handler, final_params] if handler
|
147
166
|
|
148
167
|
# Try best candidate if current failed
|
149
168
|
if state[:best_node]
|
150
169
|
best_params = state[:best_params] || params
|
151
170
|
best_captured = state[:best_captured] || captured_params
|
152
|
-
finalize_match(state[:best_node], method, best_params, best_captured)
|
153
|
-
else
|
154
|
-
result
|
171
|
+
handler, final_params = finalize_match(state[:best_node], method, best_params, best_captured)
|
155
172
|
end
|
173
|
+
|
174
|
+
[handler, final_params]
|
156
175
|
end
|
157
176
|
|
158
177
|
# Falls back to the best candidate if no exact match.
|
@@ -162,9 +181,7 @@ module RubyRoutes
|
|
162
181
|
# @param params [Hash] parameters hash
|
163
182
|
# @param captured_params [Hash] captured parameters from traversal
|
164
183
|
# @return [Array] [handler, params] or [nil, params]
|
165
|
-
|
166
|
-
finalize_match(state[:best_node], method, state[:best_params], state[:best_captured])
|
167
|
-
end
|
184
|
+
|
168
185
|
|
169
186
|
# Common method to finalize a match attempt.
|
170
187
|
# Assumes the node is already validated as an endpoint.
|
@@ -175,29 +192,27 @@ module RubyRoutes
|
|
175
192
|
# @param captured_params [Hash] captured parameters from traversal
|
176
193
|
# @return [Array] [handler, params] or [nil, params]
|
177
194
|
def finalize_match(node, method, params, captured_params)
|
195
|
+
# Apply captured params once at the beginning
|
196
|
+
apply_captured_params(params, captured_params)
|
197
|
+
|
178
198
|
if node && endpoint_with_method?(node, method)
|
179
199
|
handler = node.handlers[method]
|
180
|
-
|
181
|
-
apply_captured_params(params, captured_params)
|
182
|
-
if check_constraints(handler, params)
|
200
|
+
if check_constraints(handler, captured_params)
|
183
201
|
return [handler, params]
|
184
202
|
end
|
185
203
|
end
|
186
204
|
# For non-matching paths, return nil
|
187
|
-
apply_captured_params(params, captured_params)
|
188
205
|
[nil, params]
|
189
206
|
end
|
190
|
-
|
191
|
-
# Handles matching for the root path.
|
192
207
|
#
|
193
208
|
# @param method [String] HTTP method
|
194
209
|
# @param params_out [Hash] parameters hash
|
195
210
|
# @return [Array] [handler, params] or [nil, params]
|
196
211
|
def root_match(method, params_out)
|
197
212
|
if @root.is_endpoint && (handler = @root.handlers[method])
|
198
|
-
[handler, params_out
|
213
|
+
[handler, params_out]
|
199
214
|
else
|
200
|
-
[nil,
|
215
|
+
[nil, EMPTY_PARAMS]
|
201
216
|
end
|
202
217
|
end
|
203
218
|
|
@@ -206,7 +221,19 @@ module RubyRoutes
|
|
206
221
|
# @param params [Hash] the final parameters hash
|
207
222
|
# @param captured_params [Hash] captured parameters from traversal
|
208
223
|
def apply_captured_params(params, captured_params)
|
209
|
-
params
|
224
|
+
merge_captured_params(params, params, captured_params)
|
225
|
+
end
|
226
|
+
|
227
|
+
# Merges captured parameters into the parameter hashes.
|
228
|
+
#
|
229
|
+
# @param params [Hash] the main parameters hash
|
230
|
+
# @param captured_params [Hash] the captured parameters hash
|
231
|
+
# @param segment_captured [Hash] the newly captured parameters
|
232
|
+
def merge_captured_params(params, captured_params, segment_captured)
|
233
|
+
return if segment_captured.empty?
|
234
|
+
|
235
|
+
params.merge!(segment_captured)
|
236
|
+
captured_params.merge!(segment_captured)
|
210
237
|
end
|
211
238
|
end
|
212
239
|
end
|
@@ -5,7 +5,6 @@ module RubyRoutes
|
|
5
5
|
# Inserter module for adding routes to the RadixTree.
|
6
6
|
# Handles tokenization, node advancement, and endpoint finalization.
|
7
7
|
module Inserter
|
8
|
-
private
|
9
8
|
|
10
9
|
# Inserts a route into the RadixTree for the given path and HTTP methods.
|
11
10
|
#
|
@@ -16,7 +15,7 @@ module RubyRoutes
|
|
16
15
|
def insert_route(path_string, http_methods, route_handler)
|
17
16
|
return route_handler if path_string.nil? || path_string.empty?
|
18
17
|
|
19
|
-
tokens =
|
18
|
+
tokens = split_path_cached(path_string)
|
20
19
|
current_node = @root
|
21
20
|
tokens.each { |token| current_node = advance_node(current_node, token) }
|
22
21
|
finalize_endpoint(current_node, http_methods, route_handler)
|
@@ -29,58 +28,7 @@ module RubyRoutes
|
|
29
28
|
# @param token [String] the token to process
|
30
29
|
# @return [Node] the next node
|
31
30
|
def advance_node(current_node, token)
|
32
|
-
|
33
|
-
when ':'
|
34
|
-
handle_dynamic(current_node, token)
|
35
|
-
when '*'
|
36
|
-
handle_wildcard(current_node, token)
|
37
|
-
else
|
38
|
-
handle_static(current_node, token)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
# Handles dynamic parameter tokens (e.g., :id).
|
43
|
-
#
|
44
|
-
# @param current_node [Node] the current node
|
45
|
-
# @param token [String] the dynamic token
|
46
|
-
# @return [Node] the dynamic child node
|
47
|
-
def handle_dynamic(current_node, token)
|
48
|
-
param_name = token[1..]
|
49
|
-
current_node.dynamic_child ||= build_param_node(param_name)
|
50
|
-
current_node.dynamic_child
|
51
|
-
end
|
52
|
-
|
53
|
-
# Handles wildcard tokens (e.g., *splat).
|
54
|
-
#
|
55
|
-
# @param current_node [Node] the current node
|
56
|
-
# @param token [String] the wildcard token
|
57
|
-
# @return [Node] the wildcard child node
|
58
|
-
def handle_wildcard(current_node, token)
|
59
|
-
param_name = token[1..]
|
60
|
-
param_name = 'splat' if param_name.nil? || param_name.empty?
|
61
|
-
current_node.wildcard_child ||= build_param_node(param_name)
|
62
|
-
current_node.wildcard_child
|
63
|
-
end
|
64
|
-
|
65
|
-
# Handles static literal tokens.
|
66
|
-
#
|
67
|
-
# @param current_node [Node] the current node
|
68
|
-
# @param token [String] the static token
|
69
|
-
# @return [Node] the static child node
|
70
|
-
def handle_static(current_node, token)
|
71
|
-
literal_token = token.freeze
|
72
|
-
current_node.static_children[literal_token] ||= Node.new
|
73
|
-
current_node.static_children[literal_token]
|
74
|
-
end
|
75
|
-
|
76
|
-
# Builds a new node for parameter capture.
|
77
|
-
#
|
78
|
-
# @param param_name [String] the parameter name
|
79
|
-
# @return [Node] the new parameter node
|
80
|
-
def build_param_node(param_name)
|
81
|
-
node = Node.new
|
82
|
-
node.param_name = param_name
|
83
|
-
node
|
31
|
+
Segment.for(token).ensure_child(current_node)
|
84
32
|
end
|
85
33
|
|
86
34
|
# Finalizes the endpoint by adding handlers for HTTP methods.
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyRoutes
|
4
|
+
class RadixTree
|
5
|
+
module TraversalStrategy
|
6
|
+
# Base class for all traversal strategies.
|
7
|
+
class Base
|
8
|
+
def initialize(finder)
|
9
|
+
@finder = finder
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute(_segments, _state, _method, _params, _captured_params)
|
13
|
+
raise NotImplementedError, "#{self.class.name} must implement #execute"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module RubyRoutes
|
6
|
+
class RadixTree
|
7
|
+
module TraversalStrategy
|
8
|
+
# Traversal strategy using a generic loop, ideal for longer paths (4+ segments).
|
9
|
+
class GenericLoop < Base
|
10
|
+
def execute(segments, state, method, params, captured_params)
|
11
|
+
segments.each_with_index do |segment, index|
|
12
|
+
next_node, stop = @finder.send(:traverse_for_segment, state[:current], segment, index, segments, params, captured_params)
|
13
|
+
return @finder.send(:finalize_on_fail, state, method, params, captured_params) unless next_node
|
14
|
+
|
15
|
+
state[:current] = next_node
|
16
|
+
state[:matched] = true
|
17
|
+
@finder.send(:record_candidate, state, method, params, captured_params) if @finder.send(:endpoint_with_method?, state[:current], method)
|
18
|
+
break if stop
|
19
|
+
end
|
20
|
+
nil # Signal successful traversal
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module RubyRoutes
|
6
|
+
class RadixTree
|
7
|
+
module TraversalStrategy
|
8
|
+
# Traversal strategy using unrolled loops for common short paths (1-3 segments).
|
9
|
+
# This provides a performance boost by avoiding loop overhead for the most frequent cases.
|
10
|
+
class Unrolled < Base
|
11
|
+
def execute(segments, state, method, params, captured_params)
|
12
|
+
# This case statement manually unrolls the traversal loop for paths
|
13
|
+
# with 1, 2, or 3 segments. The `TraversalStrategy.for` factory ensures
|
14
|
+
# this strategy is only used for these lengths.
|
15
|
+
case segments.size
|
16
|
+
when 1
|
17
|
+
traverse_segment(0, segments, state, method, params, captured_params)
|
18
|
+
when 2
|
19
|
+
traverse_segment(0, segments, state, method, params, captured_params)
|
20
|
+
traverse_segment(1, segments, state, method, params, captured_params)
|
21
|
+
when 3
|
22
|
+
traverse_segment(0, segments, state, method, params, captured_params)
|
23
|
+
traverse_segment(1, segments, state, method, params, captured_params)
|
24
|
+
traverse_segment(2, segments, state, method, params, captured_params)
|
25
|
+
end
|
26
|
+
nil # Return nil to indicate successful traversal
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Traverses a single segment, updates state, and records candidate if applicable.
|
32
|
+
# Returns true if traversal should stop (e.g., due to wildcard), false otherwise.
|
33
|
+
def traverse_segment(index, segments, state, method, params, captured_params)
|
34
|
+
next_node, stop = @finder.traverse_for_segment(state[:current], segments[index], index, segments, params, captured_params)
|
35
|
+
return false unless next_node
|
36
|
+
|
37
|
+
state[:current] = next_node
|
38
|
+
state[:matched] = true
|
39
|
+
@finder.record_candidate(state, method, params, captured_params) if @finder.endpoint_with_method?(state[:current], method)
|
40
|
+
stop
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'traversal_strategy/base'
|
4
|
+
require_relative 'traversal_strategy/generic_loop'
|
5
|
+
require_relative 'traversal_strategy/unrolled'
|
6
|
+
|
7
|
+
module RubyRoutes
|
8
|
+
class RadixTree
|
9
|
+
# This module encapsulates the different strategies for traversing the RadixTree.
|
10
|
+
# It provides a factory to select the appropriate strategy based on path length.
|
11
|
+
module TraversalStrategy
|
12
|
+
# Selects the appropriate traversal strategy based on the number of segments.
|
13
|
+
#
|
14
|
+
# @param segment_count [Integer] The number of segments in the path.
|
15
|
+
# @param finder [RubyRoutes::RadixTree::Finder] The finder instance.
|
16
|
+
# @return [Base] An instance of a traversal strategy.
|
17
|
+
def self.for(segment_count, finder)
|
18
|
+
if segment_count <= 3
|
19
|
+
Unrolled.new(finder)
|
20
|
+
else
|
21
|
+
GenericLoop.new(finder)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|