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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +0 -23
  3. data/lib/ruby_routes/constant.rb +20 -3
  4. data/lib/ruby_routes/lru_strategies/hit_strategy.rb +1 -1
  5. data/lib/ruby_routes/node.rb +15 -87
  6. data/lib/ruby_routes/radix_tree/finder.rb +79 -52
  7. data/lib/ruby_routes/radix_tree/inserter.rb +2 -54
  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 +24 -1
  15. data/lib/ruby_routes/route/matcher.rb +11 -0
  16. data/lib/ruby_routes/route/param_support.rb +9 -7
  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/warning_helpers.rb +7 -5
  22. data/lib/ruby_routes/route.rb +35 -55
  23. data/lib/ruby_routes/route_set/cache_helpers.rb +32 -13
  24. data/lib/ruby_routes/route_set/collection_helpers.rb +15 -14
  25. data/lib/ruby_routes/route_set.rb +32 -69
  26. data/lib/ruby_routes/router/build_helpers.rb +1 -1
  27. data/lib/ruby_routes/router/builder.rb +16 -33
  28. data/lib/ruby_routes/router/http_helpers.rb +13 -49
  29. data/lib/ruby_routes/router/resource_helpers.rb +26 -39
  30. data/lib/ruby_routes/router/scope_helpers.rb +26 -14
  31. data/lib/ruby_routes/router.rb +41 -24
  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/static_segment.rb +3 -1
  35. data/lib/ruby_routes/strategies/base.rb +18 -0
  36. data/lib/ruby_routes/strategies/hash_based_strategy.rb +33 -0
  37. data/lib/ruby_routes/strategies/hybrid_strategy.rb +70 -0
  38. data/lib/ruby_routes/strategies/radix_tree_strategy.rb +24 -0
  39. data/lib/ruby_routes/strategies.rb +5 -0
  40. data/lib/ruby_routes/utility/key_builder_utility.rb +4 -26
  41. data/lib/ruby_routes/utility/method_utility.rb +18 -17
  42. data/lib/ruby_routes/utility/path_utility.rb +18 -7
  43. data/lib/ruby_routes/version.rb +1 -1
  44. data/lib/ruby_routes.rb +3 -1
  45. metadata +11 -2
  46. data/lib/ruby_routes/string_extensions.rb +0 -65
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 57b9470b49019746492c10fd0e202c6c544c78459f36e0d2c1f8d400103def93
4
- data.tar.gz: 8e59a14f7854cadb955d381fcf7947b7769d0041de5067f3e3dc5827d13cbb26
3
+ metadata.gz: 33b60562a702b7d2def6960af7e255d460b17e508367563aefd9ef85856abe52
4
+ data.tar.gz: be3560f98947dd313afeef38603a9f97bb6a39917d471db9a26b4dcdbb42d2bc
5
5
  SHA512:
6
- metadata.gz: c746c95407c222fdd209e464c3f1b89971987d8b41a557b7f173e1acb55fa5d6626b030bab26aed81c7ff766bd9da8c4ba4c2895ce167558a61c089923e14f12
7
- data.tar.gz: 6b512f5e810f19fa4031b33f29d3efa72feabb841e8ac3558af67ce6ec43556cacc85577993015cde7f3f400c10e9a36cbc8e007831808a798fa80204fd57046
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,7 +122,7 @@ module RubyRoutes
105
122
  # recently used entries will be evicted.
106
123
  #
107
124
  # @return [Integer]
108
- QUERY_CACHE_SIZE = 128
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, {}].freeze
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.instance_variable_get(:@hash)
33
+ store = lru.hash
34
34
  value = store.delete(key)
35
35
  store[key] = value
36
36
  value
@@ -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 = false
36
- @handlers = {}
37
- @static_children = {}
38
- @dynamic_child = nil
39
- @wildcard_child = nil
40
- @param_name = nil
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
- method_key = normalize_http_method(method)
50
- @handlers[method_key] = handler
51
- @is_endpoint = true
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
- return [@static_children[segment], false, {}] if @static_children[segment]
75
-
76
- if @dynamic_child
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, 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?
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(path)
42
- return [nil, params_out || {}] if segments.empty?
57
+ segments = split_path_cached(path_input)
58
+ return [nil, EMPTY_PARAMS] if segments.empty?
43
59
 
44
- params = params_out || {}
45
- state = traversal_state
46
- captured_params = {}
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,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, 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
- captured_params.merge!(segment_captured) if segment_captured
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.dup
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
- result = finalize_match(state[:current], method, params, captured_params)
146
- return result if result[0]
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
- def fallback_candidate(state, method, params, captured_params)
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
- # Apply captured params before constraint validation
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, params_out || {}]
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.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)
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 = 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,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
- 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
- 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