ruby_routes 2.5.0 → 2.7.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/ruby_routes/cache_setup.rb +32 -0
  4. data/lib/ruby_routes/constant.rb +18 -3
  5. data/lib/ruby_routes/lru_strategies/hit_strategy.rb +1 -6
  6. data/lib/ruby_routes/node.rb +14 -87
  7. data/lib/ruby_routes/radix_tree/finder.rb +88 -46
  8. data/lib/ruby_routes/radix_tree/inserter.rb +2 -55
  9. data/lib/ruby_routes/radix_tree/traversal_strategy/base.rb +18 -0
  10. data/lib/ruby_routes/radix_tree/traversal_strategy/generic_loop.rb +25 -0
  11. data/lib/ruby_routes/radix_tree/traversal_strategy/unrolled.rb +54 -0
  12. data/lib/ruby_routes/radix_tree/traversal_strategy.rb +26 -0
  13. data/lib/ruby_routes/radix_tree.rb +12 -62
  14. data/lib/ruby_routes/route/check_helpers.rb +2 -2
  15. data/lib/ruby_routes/route/constraint_validator.rb +24 -1
  16. data/lib/ruby_routes/route/matcher.rb +11 -0
  17. data/lib/ruby_routes/route/param_support.rb +9 -8
  18. data/lib/ruby_routes/route/path_builder.rb +11 -6
  19. data/lib/ruby_routes/route/path_generation.rb +5 -1
  20. data/lib/ruby_routes/route/small_lru.rb +51 -5
  21. data/lib/ruby_routes/route/validation_helpers.rb +6 -46
  22. data/lib/ruby_routes/route.rb +32 -59
  23. data/lib/ruby_routes/route_set/cache_helpers.rb +24 -25
  24. data/lib/ruby_routes/route_set/collection_helpers.rb +7 -16
  25. data/lib/ruby_routes/route_set.rb +36 -59
  26. data/lib/ruby_routes/router/build_helpers.rb +1 -7
  27. data/lib/ruby_routes/router/builder.rb +12 -12
  28. data/lib/ruby_routes/router/http_helpers.rb +7 -48
  29. data/lib/ruby_routes/router/resource_helpers.rb +23 -37
  30. data/lib/ruby_routes/router/scope_helpers.rb +26 -14
  31. data/lib/ruby_routes/router.rb +28 -29
  32. data/lib/ruby_routes/segment.rb +3 -3
  33. data/lib/ruby_routes/segments/base_segment.rb +8 -0
  34. data/lib/ruby_routes/segments/dynamic_segment.rb +1 -1
  35. data/lib/ruby_routes/segments/static_segment.rb +3 -1
  36. data/lib/ruby_routes/strategies/base.rb +18 -0
  37. data/lib/ruby_routes/strategies/hash_based_strategy.rb +33 -0
  38. data/lib/ruby_routes/strategies/hybrid_strategy.rb +70 -0
  39. data/lib/ruby_routes/strategies/radix_tree_strategy.rb +24 -0
  40. data/lib/ruby_routes/strategies.rb +5 -0
  41. data/lib/ruby_routes/utility/key_builder_utility.rb +4 -26
  42. data/lib/ruby_routes/utility/method_utility.rb +11 -11
  43. data/lib/ruby_routes/utility/path_utility.rb +19 -7
  44. data/lib/ruby_routes/version.rb +1 -1
  45. data/lib/ruby_routes.rb +3 -1
  46. metadata +12 -2
  47. 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: e0df861aa54b895a91e6d525f227a6c470b2d66e89e39407dded8c017f30b995
4
+ data.tar.gz: 25d99af8abfd7962e6ac6cf154ed07c2315605192b30db78a7707bf792afd1cc
5
5
  SHA512:
6
- metadata.gz: dfc84b5cfa67c1da89ab8067ad01c8706c81717611969c287a4a536055fe1ee1b4ac80b791248c05dddb090548f1e29006889d2d7f5e91b9058349b9f9a8b317
7
- data.tar.gz: b2ab1c86ca6838a4164621987c813b7320e3f042d1666576e00c3acf48576204c837fd13582ed42f2cf896334b9e1683c8ceab8868c03b7f9a482dcb808a5052
6
+ metadata.gz: 6b7f2af4f6bcffd3238e69ade25876abc785c03c601a0a91db06eb75fce14684c73f21abaf6bf2aa55b0c7f496ca682a6c5393426e802d325a740658ee9c1b1b
7
+ data.tar.gz: e11dbd0208c20b72954f1b750c475740c7678f465d2ab441bbae01a2f87d1415b2d5219ef2fdfada2f728c43e0212918bf3cc23be4ca665f9e8acbde900f6eb5
data/README.md CHANGED
@@ -36,7 +36,7 @@ A high-performance, lightweight routing system for Ruby applications providing a
36
36
  Add this line to your application's Gemfile:
37
37
 
38
38
  ```ruby
39
- gem 'ruby_routes', '~> 2.3.0'
39
+ gem 'ruby_routes', '~> 2.7.0'
40
40
  ```
41
41
 
42
42
  And then execute:
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'route/small_lru'
4
+ require_relative 'constant'
5
+
6
+ module RubyRoutes
7
+ # CacheSetup: shared module for initializing caches across Route and RouteSet.
8
+ #
9
+ # This module provides common cache setup methods to reduce duplication
10
+ # and ensure consistency in cache initialization.
11
+ module CacheSetup
12
+ attr_reader :named_routes, :small_lru, :gen_cache, :query_cache, :validation_cache,
13
+ :cache_hits, :cache_misses
14
+
15
+ # Initialize recognition caches for RouteSet.
16
+ #
17
+ # @return [void]
18
+ def setup_caches
19
+ @routes = []
20
+ @named_routes = {}
21
+ @recognition_cache = {}
22
+ @cache_mutex = Mutex.new
23
+ @cache_hits = 0
24
+ @cache_misses = 0
25
+ @recognition_cache_max = RubyRoutes::Constant::CACHE_SIZE
26
+ @small_lru = RubyRoutes::Route::SmallLru.new(RubyRoutes::Constant::CACHE_SIZE)
27
+ @gen_cache = RubyRoutes::Route::SmallLru.new(RubyRoutes::Constant::CACHE_SIZE)
28
+ @query_cache = RubyRoutes::Route::SmallLru.new(RubyRoutes::Constant::CACHE_SIZE)
29
+ @validation_cache = RubyRoutes::Route::SmallLru.new(RubyRoutes::Constant::CACHE_SIZE)
30
+ end
31
+ end
32
+ end
@@ -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'
@@ -28,12 +28,7 @@ module RubyRoutes
28
28
  # @return [Object] the cached value
29
29
  def call(lru, key)
30
30
  lru.increment_hits
31
- # Internal storage name (@hash) is intentionally accessed reflectively
32
- # to keep strategy decoupled from public API surface.
33
- store = lru.instance_variable_get(:@hash)
34
- value = store.delete(key)
35
- store[key] = value
36
- value
31
+ lru.promote(key)
37
32
  end
38
33
  end
39
34
  end
@@ -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,31 @@ 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
+
27
+ # Clears a thread-local buffer hash.
28
+ #
29
+ # @param buffer_key [Symbol] The thread-local key for the buffer.
30
+ # @return [Hash] The cleared buffer.
31
+ def clear_buffer(buffer_key)
32
+ buffer = Thread.current[buffer_key] ||= {}
33
+ buffer.clear
34
+ buffer
35
+ end
36
+
11
37
  # Evaluate constraint rules for a candidate route.
12
38
  #
13
39
  # @param route_handler [Object]
@@ -18,32 +44,34 @@ module RubyRoutes
18
44
 
19
45
  begin
20
46
  # Use a duplicate to avoid unintended mutation by validators.
21
- route_handler.validate_constraints_fast!(captured_params)
47
+ route_handler.validate_constraints_fast!(captured_params.dup)
22
48
  true
23
49
  rescue RubyRoutes::ConstraintViolation
24
50
  false
25
51
  end
26
52
  end
27
53
 
28
- private
29
-
30
54
  # Finds a route handler for the given path and HTTP method.
31
55
  #
32
56
  # @param path_input [String] the input path to match
33
57
  # @param method_input [String, Symbol] the HTTP method
34
58
  # @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
59
+ # @return [Array] [handler, params] or [nil, {}] if no match
60
+ def find(path_input, http_method, params_out = nil)
61
+ # Handle nil or empty path input
62
+ return [nil, EMPTY_PARAMS] if path_input.nil?
40
63
 
41
- segments = split_path_cached(path)
42
- return [nil, params_out || {}] if segments.empty?
64
+ method = normalize_http_method(http_method)
65
+ return root_match(method, params_out || EMPTY_PARAMS) if path_input.empty? || path_input == RubyRoutes::Constant::ROOT_PATH
43
66
 
44
- params = params_out || {}
45
- state = traversal_state
46
- captured_params = {}
67
+ segments = split_path_cached(path_input)
68
+ return [nil, EMPTY_PARAMS] if segments.empty?
69
+
70
+ # Clear and acquire thread-local buffers to avoid new hash creation
71
+ params = clear_buffer(PARAMS_BUFFER_KEY)
72
+ params.merge!(params_out) if params_out
73
+ captured_params = clear_buffer(CAPTURED_PARAMS_BUFFER_KEY)
74
+ state = acquire_state_buffer # Already clears internally
47
75
 
48
76
  result = perform_traversal(segments, state, method, params, captured_params)
49
77
  return result unless result.nil?
@@ -54,17 +82,31 @@ module RubyRoutes
54
82
  # Initializes the traversal state for route matching.
55
83
  #
56
84
  # @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
- }
85
+ def acquire_state_buffer
86
+ state = clear_buffer(STATE_BUFFER_KEY) # Use clear_buffer for consistency
87
+ state[:current] = @root
88
+ state[:best_node] = nil
89
+ state[:best_params] = nil
90
+ state[:best_captured] = nil
91
+ state[:matched] = false
92
+ state
93
+ end
94
+
95
+ def acquire_params_buffer(initial_params)
96
+ buffer = Thread.current[PARAMS_BUFFER_KEY] ||= {}
97
+ buffer.clear
98
+ buffer.merge!(initial_params) if initial_params
99
+ buffer
100
+ end
101
+
102
+ def acquire_captured_params_buffer
103
+ buffer = Thread.current[CAPTURED_PARAMS_BUFFER_KEY] ||= {}
104
+ buffer.clear
105
+ buffer
65
106
  end
66
107
 
67
108
  # Performs traversal through path segments to find a matching route.
109
+ # Optimized for common cases of 1-3 segments.
68
110
  #
69
111
  # @param segments [Array<String>] path segments
70
112
  # @param state [Hash] traversal state
@@ -72,17 +114,8 @@ module RubyRoutes
72
114
  # @param params [Hash] parameters hash
73
115
  # @param captured_params [Hash] hash to collect captured parameters
74
116
  # @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
117
+ def perform_traversal(segments, state, method, params, captured_params) # rubocop:disable Metrics/AbcSize
118
+ TraversalStrategy.for(segments.size, self).execute(segments, state, method, params, captured_params)
86
119
  end
87
120
 
88
121
  # Traverses to the next node for a given segment.
@@ -93,13 +126,10 @@ module RubyRoutes
93
126
  # @param segments [Array<String>] all segments
94
127
  # @param params [Hash] parameters hash
95
128
  # @param captured_params [Hash] hash to collect captured parameters
96
- # @return [Array] [next_node, stop_traversal, segment_captured]
129
+ # @return [Array] [next_node, stop_traversal]
97
130
  def traverse_for_segment(node, segment, index, segments, params, captured_params)
98
131
  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
132
+ merge_captured_params(params, captured_params, segment_captured)
103
133
  [next_node, stop]
104
134
  end
105
135
 
@@ -145,17 +175,17 @@ module RubyRoutes
145
175
  # @param captured_params [Hash] captured parameters from traversal
146
176
  # @return [Array] [handler, params] or [nil, params]
147
177
  def finalize_success(state, method, params, captured_params)
148
- result = finalize_match(state[:current], method, params, captured_params)
149
- return result if result[0]
178
+ handler, final_params = finalize_match(state[:current], method, params, captured_params)
179
+ return [handler, final_params] if handler
150
180
 
151
181
  # Try best candidate if current failed
152
182
  if state[:best_node]
153
183
  best_params = state[:best_params] || params
154
184
  best_captured = state[:best_captured] || captured_params
155
- finalize_match(state[:best_node], method, best_params, best_captured)
156
- else
157
- result
185
+ handler, final_params = finalize_match(state[:best_node], method, best_params, best_captured)
158
186
  end
187
+
188
+ [handler, final_params]
159
189
  end
160
190
 
161
191
  # Falls back to the best candidate if no exact match.
@@ -181,7 +211,7 @@ module RubyRoutes
181
211
 
182
212
  if node && endpoint_with_method?(node, method)
183
213
  handler = node.handlers[method]
184
- if check_constraints(handler, params)
214
+ if check_constraints(handler, captured_params)
185
215
  return [handler, params]
186
216
  end
187
217
  end
@@ -194,9 +224,9 @@ module RubyRoutes
194
224
  # @return [Array] [handler, params] or [nil, params]
195
225
  def root_match(method, params_out)
196
226
  if @root.is_endpoint && (handler = @root.handlers[method])
197
- [handler, params_out || {}]
227
+ [handler, params_out]
198
228
  else
199
- [nil, params_out || {}]
229
+ [nil, EMPTY_PARAMS]
200
230
  end
201
231
  end
202
232
 
@@ -205,7 +235,19 @@ module RubyRoutes
205
235
  # @param params [Hash] the final parameters hash
206
236
  # @param captured_params [Hash] captured parameters from traversal
207
237
  def apply_captured_params(params, captured_params)
208
- params.merge!(captured_params) if captured_params && !captured_params.empty?
238
+ merge_captured_params(params, params, captured_params)
239
+ end
240
+
241
+ # Merges captured parameters into the parameter hashes.
242
+ #
243
+ # @param params [Hash] the main parameters hash
244
+ # @param captured_params [Hash] the captured parameters hash
245
+ # @param segment_captured [Hash] the newly captured parameters
246
+ def merge_captured_params(params, captured_params, segment_captured)
247
+ return if segment_captured.empty?
248
+
249
+ params.merge!(segment_captured)
250
+ captured_params.merge!(segment_captured)
209
251
  end
210
252
  end
211
253
  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,54 @@
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
+ outcome = traverse_segment(0, segments, state, method, params, captured_params)
18
+ return @finder.finalize_on_fail(state, method, params, captured_params) if outcome == :fail
19
+ when 2
20
+ outcome = traverse_segment(0, segments, state, method, params, captured_params)
21
+ return @finder.finalize_on_fail(state, method, params, captured_params) if outcome == :fail
22
+ return nil if outcome == true # stop
23
+ outcome = traverse_segment(1, segments, state, method, params, captured_params)
24
+ return @finder.finalize_on_fail(state, method, params, captured_params) if outcome == :fail
25
+ when 3
26
+ outcome = traverse_segment(0, segments, state, method, params, captured_params)
27
+ return @finder.finalize_on_fail(state, method, params, captured_params) if outcome == :fail
28
+ return nil if outcome == true # stop
29
+ outcome = traverse_segment(1, segments, state, method, params, captured_params)
30
+ return @finder.finalize_on_fail(state, method, params, captured_params) if outcome == :fail
31
+ return nil if outcome == true # stop
32
+ outcome = traverse_segment(2, segments, state, method, params, captured_params)
33
+ return @finder.finalize_on_fail(state, method, params, captured_params) if outcome == :fail
34
+ end
35
+ nil # Return nil to indicate successful traversal
36
+ end
37
+
38
+ private
39
+
40
+ # Traverses a single segment, updates state, and records candidate if applicable.
41
+ # Returns true if traversal should stop (e.g., due to wildcard), false otherwise.
42
+ def traverse_segment(index, segments, state, method, params, captured_params)
43
+ next_node, stop = @finder.traverse_for_segment(state[:current], segments[index], index, segments, params, captured_params)
44
+ return :fail unless next_node
45
+
46
+ state[:current] = next_node
47
+ state[:matched] = true
48
+ @finder.record_candidate(state, method, params, captured_params) if @finder.endpoint_with_method?(state[:current], method)
49
+ stop
50
+ end
51
+ end
52
+ end
53
+ end
54
+ 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