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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +0 -23
  3. data/lib/ruby_routes/constant.rb +18 -3
  4. data/lib/ruby_routes/lru_strategies/hit_strategy.rb +1 -1
  5. data/lib/ruby_routes/node.rb +14 -87
  6. data/lib/ruby_routes/radix_tree/finder.rb +75 -47
  7. data/lib/ruby_routes/radix_tree/inserter.rb +2 -55
  8. data/lib/ruby_routes/radix_tree/traversal_strategy/base.rb +18 -0
  9. data/lib/ruby_routes/radix_tree/traversal_strategy/generic_loop.rb +25 -0
  10. data/lib/ruby_routes/radix_tree/traversal_strategy/unrolled.rb +45 -0
  11. data/lib/ruby_routes/radix_tree/traversal_strategy.rb +26 -0
  12. data/lib/ruby_routes/radix_tree.rb +12 -62
  13. data/lib/ruby_routes/route/check_helpers.rb +3 -3
  14. data/lib/ruby_routes/route/constraint_validator.rb +22 -1
  15. data/lib/ruby_routes/route/matcher.rb +11 -0
  16. data/lib/ruby_routes/route/param_support.rb +9 -8
  17. data/lib/ruby_routes/route/path_builder.rb +11 -6
  18. data/lib/ruby_routes/route/path_generation.rb +5 -1
  19. data/lib/ruby_routes/route/small_lru.rb +43 -2
  20. data/lib/ruby_routes/route/validation_helpers.rb +6 -36
  21. data/lib/ruby_routes/route.rb +35 -56
  22. data/lib/ruby_routes/route_set/cache_helpers.rb +29 -13
  23. data/lib/ruby_routes/route_set/collection_helpers.rb +8 -10
  24. data/lib/ruby_routes/route_set.rb +34 -59
  25. data/lib/ruby_routes/router/build_helpers.rb +1 -7
  26. data/lib/ruby_routes/router/builder.rb +12 -12
  27. data/lib/ruby_routes/router/http_helpers.rb +7 -48
  28. data/lib/ruby_routes/router/resource_helpers.rb +23 -37
  29. data/lib/ruby_routes/router/scope_helpers.rb +26 -14
  30. data/lib/ruby_routes/router.rb +28 -29
  31. data/lib/ruby_routes/segment.rb +3 -3
  32. data/lib/ruby_routes/segments/base_segment.rb +8 -0
  33. data/lib/ruby_routes/segments/static_segment.rb +3 -1
  34. data/lib/ruby_routes/strategies/base.rb +18 -0
  35. data/lib/ruby_routes/strategies/hash_based_strategy.rb +33 -0
  36. data/lib/ruby_routes/strategies/hybrid_strategy.rb +70 -0
  37. data/lib/ruby_routes/strategies/radix_tree_strategy.rb +24 -0
  38. data/lib/ruby_routes/strategies.rb +5 -0
  39. data/lib/ruby_routes/utility/key_builder_utility.rb +4 -26
  40. data/lib/ruby_routes/utility/method_utility.rb +11 -11
  41. data/lib/ruby_routes/utility/path_utility.rb +18 -7
  42. data/lib/ruby_routes/version.rb +1 -1
  43. data/lib/ruby_routes.rb +3 -1
  44. metadata +11 -2
  45. data/lib/ruby_routes/string_extensions.rb +0 -65
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 07fa3c9d3d13ac8b73c6f4775871cd5ad6c3cd7c5d0e571797b83fb0dea72189
4
- data.tar.gz: 2684722163772fe4489d4e8c32eaff036002c5b58e05be68e25e9a0c59cd680b
3
+ metadata.gz: 33b60562a702b7d2def6960af7e255d460b17e508367563aefd9ef85856abe52
4
+ data.tar.gz: be3560f98947dd313afeef38603a9f97bb6a39917d471db9a26b4dcdbb42d2bc
5
5
  SHA512:
6
- metadata.gz: dfc84b5cfa67c1da89ab8067ad01c8706c81717611969c287a4a536055fe1ee1b4ac80b791248c05dddb090548f1e29006889d2d7f5e91b9058349b9f9a8b317
7
- data.tar.gz: b2ab1c86ca6838a4164621987c813b7320e3f042d1666576e00c3acf48576204c837fd13582ed42f2cf896334b9e1683c8ceab8868c03b7f9a482dcb808a5052
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:
@@ -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
- QUERY_CACHE_SIZE = 128
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.instance_variable_get(:@hash)
33
+ store = lru.hash
34
34
  value = store.delete(key)
35
35
  store[key] = value
36
36
  value
@@ -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 = false
37
- @handlers = {}
38
- @static_children = {}
39
- @dynamic_child = nil
40
- @wildcard_child = nil
41
- @param_name = nil
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
- method_key = normalize_http_method(method)
51
- @handlers[method_key] = handler
52
- @is_endpoint = true
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
- return [@static_children[segment], false, {}] if @static_children[segment]
76
-
77
- if @dynamic_child
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, params] if no match
36
- def find(path_input, method_input, params_out = nil)
37
- path = path_input.to_s
38
- method = normalize_http_method(method_input)
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
- segments = split_path_cached(path)
42
- return [nil, params_out || {}] if segments.empty?
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
- params = params_out || {}
45
- state = traversal_state
46
- captured_params = {}
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 traversal_state
58
- {
59
- current: @root,
60
- best_node: nil,
61
- best_params: nil,
62
- best_captured: nil,
63
- matched: false # Track if any segment was successfully matched
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.each_with_index do |segment, index|
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, segment_captured]
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
- if segment_captured
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.dup
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
- result = finalize_match(state[:current], method, params, captured_params)
149
- return result if result[0]
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, params)
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, params_out || {}]
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.merge!(captured_params) if captured_params && !captured_params.empty?
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 = split_path(path_string)
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
- case token[0]
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