ruby_routes 2.5.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 +18 -3
- data/lib/ruby_routes/lru_strategies/hit_strategy.rb +1 -1
- data/lib/ruby_routes/node.rb +14 -87
- data/lib/ruby_routes/radix_tree/finder.rb +75 -47
- data/lib/ruby_routes/radix_tree/inserter.rb +2 -55
- 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 +22 -1
- data/lib/ruby_routes/route/matcher.rb +11 -0
- data/lib/ruby_routes/route/param_support.rb +9 -8
- 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.rb +35 -56
- data/lib/ruby_routes/route_set/cache_helpers.rb +29 -13
- data/lib/ruby_routes/route_set/collection_helpers.rb +8 -10
- data/lib/ruby_routes/route_set.rb +34 -59
- data/lib/ruby_routes/router/build_helpers.rb +1 -7
- data/lib/ruby_routes/router/builder.rb +12 -12
- data/lib/ruby_routes/router/http_helpers.rb +7 -48
- data/lib/ruby_routes/router/resource_helpers.rb +23 -37
- data/lib/ruby_routes/router/scope_helpers.rb +26 -14
- data/lib/ruby_routes/router.rb +28 -29
- 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 +11 -11
- 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,9 +122,7 @@ module RubyRoutes
|
|
105
122
|
# recently used entries will be evicted.
|
106
123
|
#
|
107
124
|
# @return [Integer]
|
108
|
-
|
109
|
-
|
110
|
-
METHOD_CACHE_MAX_SIZE = 1000
|
125
|
+
CACHE_SIZE = 2048
|
111
126
|
|
112
127
|
# HTTP method constants.
|
113
128
|
HTTP_GET = 'GET'
|
@@ -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,115 +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'
|
6
5
|
require_relative 'constant'
|
7
6
|
|
8
7
|
module RubyRoutes
|
9
|
-
# Node
|
10
|
-
#
|
11
|
-
# A single vertex in the routing radix tree.
|
12
|
-
#
|
13
|
-
# Structure:
|
14
|
-
# - static_children: Hash<String, Node> exact literal matches.
|
15
|
-
# - dynamic_child: Node (":param") matches any single segment, captures value.
|
16
|
-
# - wildcard_child: Node ("*splat") matches remaining segments (greedy).
|
17
|
-
#
|
18
|
-
# Handlers:
|
19
|
-
# - @handlers maps canonical HTTP method strings (e.g. "GET") to Route objects (or callable handlers).
|
20
|
-
# - @is_endpoint marks that at least one handler is attached (terminal path).
|
21
|
-
#
|
22
|
-
# Matching precedence (most → least specific):
|
23
|
-
# static → dynamic → wildcard
|
24
|
-
#
|
25
|
-
# Thread safety: not thread-safe (build during boot).
|
26
|
-
#
|
27
|
-
# @api internal
|
28
8
|
class Node
|
9
|
+
include RubyRoutes::Utility::MethodUtility
|
10
|
+
|
29
11
|
attr_accessor :param_name, :is_endpoint, :dynamic_child, :wildcard_child
|
30
12
|
attr_reader :handlers, :static_children
|
31
13
|
|
32
|
-
include RubyRoutes::Utility::PathUtility
|
33
|
-
include RubyRoutes::Utility::MethodUtility
|
34
|
-
|
35
14
|
def initialize
|
36
|
-
@is_endpoint
|
37
|
-
@handlers
|
38
|
-
@static_children
|
39
|
-
@dynamic_child
|
40
|
-
@wildcard_child
|
41
|
-
@param_name
|
15
|
+
@is_endpoint = false
|
16
|
+
@handlers = {}
|
17
|
+
@static_children = {}
|
18
|
+
@dynamic_child = nil
|
19
|
+
@wildcard_child = nil
|
20
|
+
@param_name = nil
|
42
21
|
end
|
43
22
|
|
44
|
-
# Register a handler under an HTTP method.
|
45
|
-
#
|
46
|
-
# @param method [String, Symbol]
|
47
|
-
# @param handler [Object] route or callable
|
48
|
-
# @return [Object] handler
|
49
23
|
def add_handler(method, handler)
|
50
|
-
|
51
|
-
@handlers[
|
52
|
-
@is_endpoint
|
53
|
-
handler
|
24
|
+
method_str = normalize_http_method(method)
|
25
|
+
@handlers[method_str] = handler
|
26
|
+
@is_endpoint = true
|
54
27
|
end
|
55
28
|
|
56
|
-
# Fetch a handler for a method.
|
57
|
-
#
|
58
|
-
# @param method [String, Symbol]
|
59
|
-
# @return [Object, nil]
|
60
29
|
def get_handler(method)
|
61
30
|
@handlers[normalize_http_method(method)]
|
62
31
|
end
|
63
32
|
|
64
|
-
# Traverses from this node using a single path segment.
|
65
|
-
# Returns [next_node_or_nil, stop_traversal(Boolean), captured_params(Hash)].
|
66
|
-
#
|
67
|
-
# Optimized + simplified (cyclomatic / perceived complexity, length).
|
68
|
-
#
|
69
|
-
# @param segment [String] the path segment to match
|
70
|
-
# @param index [Integer] the segment index in the full path
|
71
|
-
# @param segments [Array<String>] all path segments
|
72
|
-
# @param params [Hash] (unused in this method; mutation deferred)
|
73
|
-
# @return [Array] [next_node, stop, captured] or NO_TRAVERSAL_RESULT
|
74
33
|
def traverse_for(segment, index, segments, params)
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
captured = capture_dynamic_param(@dynamic_child, segment)
|
79
|
-
return [@dynamic_child, false, captured]
|
80
|
-
end
|
81
|
-
|
82
|
-
if @wildcard_child
|
83
|
-
captured = capture_wildcard_param(@wildcard_child, segments, index)
|
84
|
-
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
|
85
37
|
end
|
86
38
|
|
87
39
|
RubyRoutes::Constant::NO_TRAVERSAL_RESULT
|
88
40
|
end
|
89
|
-
|
90
|
-
private
|
91
|
-
|
92
|
-
# Captures a dynamic parameter value and returns it for later assignment.
|
93
|
-
#
|
94
|
-
# @param dynamic_node [Node] the dynamic child node
|
95
|
-
# @param value [String] the segment value to capture
|
96
|
-
# @return [Hash] captured parameter hash or empty hash if no param
|
97
|
-
def capture_dynamic_param(dynamic_node, value)
|
98
|
-
return {} unless dynamic_node.param_name
|
99
|
-
|
100
|
-
{ dynamic_node.param_name => value }
|
101
|
-
end
|
102
|
-
|
103
|
-
# Captures a wildcard parameter value and returns it for later assignment.
|
104
|
-
#
|
105
|
-
# @param wc_node [Node] the wildcard child node
|
106
|
-
# @param segments [Array<String>] the full path segments
|
107
|
-
# @param index [Integer] the current segment index
|
108
|
-
# @return [Hash] captured parameter hash or empty hash if no param
|
109
|
-
def capture_wildcard_param(wildcard_node, segments, index)
|
110
|
-
return {} unless wildcard_node.param_name
|
111
|
-
|
112
|
-
{ wildcard_node.param_name => segments[index..].join('/') }
|
113
|
-
end
|
114
41
|
end
|
115
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
|
-
return root_match(method, params_out) if path.empty? || path == RubyRoutes::Constant::ROOT_PATH
|
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?
|
40
53
|
|
41
|
-
|
42
|
-
return
|
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
|
43
56
|
|
44
|
-
|
45
|
-
|
46
|
-
|
57
|
+
segments = split_path_cached(path_input)
|
58
|
+
return [nil, EMPTY_PARAMS] if segments.empty?
|
59
|
+
|
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,13 +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
|
-
|
100
|
-
params.merge!(segment_captured) # Merge into running params hash at each step
|
101
|
-
captured_params.merge!(segment_captured) # Keep for best candidate consistency
|
102
|
-
end
|
118
|
+
merge_captured_params(params, captured_params, segment_captured)
|
103
119
|
[next_node, stop]
|
104
120
|
end
|
105
121
|
|
@@ -111,7 +127,7 @@ module RubyRoutes
|
|
111
127
|
# @param captured_params [Hash] captured parameters from traversal
|
112
128
|
def record_candidate(state, _method, params, captured_params)
|
113
129
|
state[:best_node] = state[:current]
|
114
|
-
state[:best_params] = params
|
130
|
+
state[:best_params] = params
|
115
131
|
state[:best_captured] = captured_params.dup
|
116
132
|
end
|
117
133
|
|
@@ -145,17 +161,17 @@ module RubyRoutes
|
|
145
161
|
# @param captured_params [Hash] captured parameters from traversal
|
146
162
|
# @return [Array] [handler, params] or [nil, params]
|
147
163
|
def finalize_success(state, method, params, captured_params)
|
148
|
-
|
149
|
-
return
|
164
|
+
handler, final_params = finalize_match(state[:current], method, params, captured_params)
|
165
|
+
return [handler, final_params] if handler
|
150
166
|
|
151
167
|
# Try best candidate if current failed
|
152
168
|
if state[:best_node]
|
153
169
|
best_params = state[:best_params] || params
|
154
170
|
best_captured = state[:best_captured] || captured_params
|
155
|
-
finalize_match(state[:best_node], method, best_params, best_captured)
|
156
|
-
else
|
157
|
-
result
|
171
|
+
handler, final_params = finalize_match(state[:best_node], method, best_params, best_captured)
|
158
172
|
end
|
173
|
+
|
174
|
+
[handler, final_params]
|
159
175
|
end
|
160
176
|
|
161
177
|
# Falls back to the best candidate if no exact match.
|
@@ -181,7 +197,7 @@ module RubyRoutes
|
|
181
197
|
|
182
198
|
if node && endpoint_with_method?(node, method)
|
183
199
|
handler = node.handlers[method]
|
184
|
-
if check_constraints(handler,
|
200
|
+
if check_constraints(handler, captured_params)
|
185
201
|
return [handler, params]
|
186
202
|
end
|
187
203
|
end
|
@@ -194,9 +210,9 @@ module RubyRoutes
|
|
194
210
|
# @return [Array] [handler, params] or [nil, params]
|
195
211
|
def root_match(method, params_out)
|
196
212
|
if @root.is_endpoint && (handler = @root.handlers[method])
|
197
|
-
[handler, params_out
|
213
|
+
[handler, params_out]
|
198
214
|
else
|
199
|
-
[nil,
|
215
|
+
[nil, EMPTY_PARAMS]
|
200
216
|
end
|
201
217
|
end
|
202
218
|
|
@@ -205,7 +221,19 @@ module RubyRoutes
|
|
205
221
|
# @param params [Hash] the final parameters hash
|
206
222
|
# @param captured_params [Hash] captured parameters from traversal
|
207
223
|
def apply_captured_params(params, captured_params)
|
208
|
-
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)
|
209
237
|
end
|
210
238
|
end
|
211
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,59 +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
|
-
raise ArgumentError, "Dynamic parameter name cannot be empty" if param_name.nil? || param_name.empty?
|
50
|
-
current_node.dynamic_child ||= build_param_node(param_name)
|
51
|
-
current_node.dynamic_child
|
52
|
-
end
|
53
|
-
|
54
|
-
# Handles wildcard tokens (e.g., *splat).
|
55
|
-
#
|
56
|
-
# @param current_node [Node] the current node
|
57
|
-
# @param token [String] the wildcard token
|
58
|
-
# @return [Node] the wildcard child node
|
59
|
-
def handle_wildcard(current_node, token)
|
60
|
-
param_name = token[1..]
|
61
|
-
param_name = 'splat' if param_name.nil? || param_name.empty?
|
62
|
-
current_node.wildcard_child ||= build_param_node(param_name)
|
63
|
-
current_node.wildcard_child
|
64
|
-
end
|
65
|
-
|
66
|
-
# Handles static literal tokens.
|
67
|
-
#
|
68
|
-
# @param current_node [Node] the current node
|
69
|
-
# @param token [String] the static token
|
70
|
-
# @return [Node] the static child node
|
71
|
-
def handle_static(current_node, token)
|
72
|
-
literal_token = token.freeze
|
73
|
-
current_node.static_children[literal_token] ||= Node.new
|
74
|
-
current_node.static_children[literal_token]
|
75
|
-
end
|
76
|
-
|
77
|
-
# Builds a new node for parameter capture.
|
78
|
-
#
|
79
|
-
# @param param_name [String] the parameter name
|
80
|
-
# @return [Node] the new parameter node
|
81
|
-
def build_param_node(param_name)
|
82
|
-
node = Node.new
|
83
|
-
node.param_name = param_name
|
84
|
-
node
|
31
|
+
Segment.for(token).ensure_child(current_node)
|
85
32
|
end
|
86
33
|
|
87
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
|