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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/ruby_routes/cache_setup.rb +32 -0
- data/lib/ruby_routes/constant.rb +18 -3
- data/lib/ruby_routes/lru_strategies/hit_strategy.rb +1 -6
- data/lib/ruby_routes/node.rb +14 -87
- data/lib/ruby_routes/radix_tree/finder.rb +88 -46
- 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 +54 -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 +2 -2
- data/lib/ruby_routes/route/constraint_validator.rb +24 -1
- data/lib/ruby_routes/route/matcher.rb +11 -0
- data/lib/ruby_routes/route/param_support.rb +9 -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 +51 -5
- data/lib/ruby_routes/route/validation_helpers.rb +6 -46
- data/lib/ruby_routes/route.rb +32 -59
- data/lib/ruby_routes/route_set/cache_helpers.rb +24 -25
- data/lib/ruby_routes/route_set/collection_helpers.rb +7 -16
- data/lib/ruby_routes/route_set.rb +36 -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/dynamic_segment.rb +1 -1
- 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 +19 -7
- data/lib/ruby_routes/version.rb +1 -1
- data/lib/ruby_routes.rb +3 -1
- metadata +12 -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: e0df861aa54b895a91e6d525f227a6c470b2d66e89e39407dded8c017f30b995
|
4
|
+
data.tar.gz: 25d99af8abfd7962e6ac6cf154ed07c2315605192b30db78a7707bf792afd1cc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b7f2af4f6bcffd3238e69ade25876abc785c03c601a0a91db06eb75fce14684c73f21abaf6bf2aa55b0c7f496ca682a6c5393426e802d325a740658ee9c1b1b
|
7
|
+
data.tar.gz: e11dbd0208c20b72954f1b750c475740c7678f465d2ab441bbae01a2f87d1415b2d5219ef2fdfada2f728c43e0212918bf3cc23be4ca665f9e8acbde900f6eb5
|
data/README.md
CHANGED
@@ -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
|
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'
|
@@ -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
|
-
|
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
|
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,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,
|
36
|
-
def find(path_input,
|
37
|
-
path
|
38
|
-
|
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
|
-
|
42
|
-
return
|
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
|
-
|
45
|
-
|
46
|
-
|
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
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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.
|
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
|
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
|
-
|
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
|
-
|
149
|
-
return
|
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,
|
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,
|
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
|
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 =
|
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,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
|