ruby_routes 2.2.0 → 2.4.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 +240 -163
- data/lib/ruby_routes/constant.rb +137 -18
- data/lib/ruby_routes/lru_strategies/hit_strategy.rb +31 -4
- data/lib/ruby_routes/lru_strategies/miss_strategy.rb +21 -0
- data/lib/ruby_routes/node.rb +86 -36
- data/lib/ruby_routes/radix_tree/finder.rb +213 -0
- data/lib/ruby_routes/radix_tree/inserter.rb +96 -0
- data/lib/ruby_routes/radix_tree.rb +65 -230
- data/lib/ruby_routes/route/check_helpers.rb +115 -0
- data/lib/ruby_routes/route/constraint_validator.rb +173 -0
- data/lib/ruby_routes/route/param_support.rb +200 -0
- data/lib/ruby_routes/route/path_builder.rb +84 -0
- data/lib/ruby_routes/route/path_generation.rb +87 -0
- data/lib/ruby_routes/route/query_helpers.rb +56 -0
- data/lib/ruby_routes/route/segment_compiler.rb +166 -0
- data/lib/ruby_routes/route/small_lru.rb +93 -18
- data/lib/ruby_routes/route/validation_helpers.rb +174 -0
- data/lib/ruby_routes/route/warning_helpers.rb +57 -0
- data/lib/ruby_routes/route.rb +127 -501
- data/lib/ruby_routes/route_set/cache_helpers.rb +76 -0
- data/lib/ruby_routes/route_set/collection_helpers.rb +125 -0
- data/lib/ruby_routes/route_set.rb +140 -132
- data/lib/ruby_routes/router/build_helpers.rb +99 -0
- data/lib/ruby_routes/router/builder.rb +97 -0
- data/lib/ruby_routes/router/http_helpers.rb +135 -0
- data/lib/ruby_routes/router/resource_helpers.rb +137 -0
- data/lib/ruby_routes/router/scope_helpers.rb +127 -0
- data/lib/ruby_routes/router.rb +196 -182
- data/lib/ruby_routes/segment.rb +28 -8
- data/lib/ruby_routes/segments/base_segment.rb +40 -4
- data/lib/ruby_routes/segments/dynamic_segment.rb +48 -12
- data/lib/ruby_routes/segments/static_segment.rb +43 -7
- data/lib/ruby_routes/segments/wildcard_segment.rb +58 -12
- data/lib/ruby_routes/string_extensions.rb +52 -15
- data/lib/ruby_routes/url_helpers.rb +106 -24
- data/lib/ruby_routes/utility/inflector_utility.rb +35 -0
- data/lib/ruby_routes/utility/key_builder_utility.rb +171 -77
- data/lib/ruby_routes/utility/method_utility.rb +137 -0
- data/lib/ruby_routes/utility/path_utility.rb +75 -28
- data/lib/ruby_routes/utility/route_utility.rb +30 -2
- data/lib/ruby_routes/version.rb +3 -1
- data/lib/ruby_routes.rb +68 -11
- metadata +27 -7
data/lib/ruby_routes/constant.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'segments/base_segment'
|
2
4
|
require_relative 'segments/dynamic_segment'
|
3
5
|
require_relative 'segments/static_segment'
|
@@ -6,13 +8,47 @@ require_relative 'lru_strategies/hit_strategy'
|
|
6
8
|
require_relative 'lru_strategies/miss_strategy'
|
7
9
|
|
8
10
|
module RubyRoutes
|
11
|
+
# Constant
|
12
|
+
#
|
13
|
+
# Central registry for lightweight immutable structures and singleton
|
14
|
+
# strategy objects used across routing components. Centralization keeps
|
15
|
+
# hot path code free of repeated allocations and magic numbers.
|
16
|
+
#
|
17
|
+
# Responsibilities:
|
18
|
+
# - Map the first byte (ASCII) of a raw segment to its Segment subclass.
|
19
|
+
# - Provide lambda matchers for radix traversal (legacy/fallback).
|
20
|
+
# - Expose singleton LRU strategy objects (hit/miss).
|
21
|
+
# - Build compact Hash descriptors for parsed route path segments.
|
22
|
+
#
|
23
|
+
# Design Notes:
|
24
|
+
# - Numeric keys (42, 58) are ASCII codes for '*' and ':' allowing
|
25
|
+
# O(1) dispatch without extra string comparisons.
|
26
|
+
# - Descriptor factories return frozen data to enable safe reuse.
|
27
|
+
#
|
28
|
+
# @api internal
|
9
29
|
module Constant
|
30
|
+
# Shared, canonical root path constant (single source of truth).
|
31
|
+
ROOT_PATH = '/'.freeze
|
32
|
+
|
33
|
+
# Maps a segment's first byte (ASCII) to a Segment class.
|
34
|
+
#
|
35
|
+
# Keys:
|
36
|
+
# - 42 ('*') -> Wildcard
|
37
|
+
# - 58 (':') -> Dynamic
|
38
|
+
# - :default -> Static
|
39
|
+
#
|
40
|
+
# @return [Hash{Integer, Symbol => Class}]
|
10
41
|
SEGMENTS = {
|
11
42
|
42 => RubyRoutes::Segments::WildcardSegment, # '*'
|
12
43
|
58 => RubyRoutes::Segments::DynamicSegment, # ':'
|
13
44
|
:default => RubyRoutes::Segments::StaticSegment
|
14
45
|
}.freeze
|
15
46
|
|
47
|
+
# Legacy lambda-based segment matchers (kept for compatibility/fallback).
|
48
|
+
#
|
49
|
+
# Each lambda returns `[next_node, stop_traversal]` or `nil` when no match.
|
50
|
+
#
|
51
|
+
# @return [Hash{Symbol => Proc}]
|
16
52
|
SEGMENT_MATCHERS = {
|
17
53
|
static: lambda do |node, segment, _idx, _segments, _params|
|
18
54
|
child = node.static_children[segment]
|
@@ -21,38 +57,121 @@ module RubyRoutes
|
|
21
57
|
|
22
58
|
dynamic: lambda do |node, segment, _idx, _segments, params|
|
23
59
|
return nil unless node.dynamic_child
|
24
|
-
|
25
|
-
|
26
|
-
[
|
60
|
+
|
61
|
+
next_node = node.dynamic_child
|
62
|
+
params[next_node.param_name.to_s] = segment if params && next_node.param_name
|
63
|
+
[next_node, false]
|
27
64
|
end,
|
28
65
|
|
29
66
|
wildcard: lambda do |node, _segment, idx, segments, params|
|
30
67
|
return nil unless node.wildcard_child
|
31
|
-
|
32
|
-
|
33
|
-
[
|
68
|
+
|
69
|
+
next_node = node.wildcard_child
|
70
|
+
params[next_node.param_name.to_s] = segments[idx..].join('/') if params && next_node.param_name
|
71
|
+
[next_node, true]
|
34
72
|
end,
|
35
73
|
|
36
|
-
|
37
|
-
default: lambda { |_node, _segment, _idx, _segments, _params| nil }
|
74
|
+
default: ->(_node, _segment, _idx, _segments, _params) { nil }
|
38
75
|
}.freeze
|
39
76
|
|
40
|
-
#
|
41
|
-
|
77
|
+
# Singleton instances to avoid per-cache strategy allocations.
|
78
|
+
#
|
79
|
+
# @return [RubyRoutes::LruStrategies::HitStrategy, RubyRoutes::LruStrategies::MissStrategy]
|
80
|
+
LRU_HIT_STRATEGY = RubyRoutes::LruStrategies::HitStrategy.new.freeze
|
42
81
|
LRU_MISS_STRATEGY = RubyRoutes::LruStrategies::MissStrategy.new.freeze
|
43
82
|
|
44
|
-
#
|
83
|
+
# Factories producing compact immutable descriptors for segments used
|
84
|
+
# during route compilation (faster than instantiating many objects).
|
85
|
+
#
|
86
|
+
# @return [Hash{Integer, Symbol => Proc}]
|
45
87
|
DESCRIPTOR_FACTORIES = {
|
46
|
-
42 =>
|
47
|
-
|
48
|
-
|
88
|
+
42 => lambda { |s|
|
89
|
+
name = s[1..]
|
90
|
+
{ type: :splat, name: (name.nil? || name.empty? ? 'splat' : name).freeze }
|
91
|
+
}, # '*'
|
92
|
+
58 => ->(s) { { type: :param, name: s[1..].freeze } }, # ':'
|
93
|
+
:default => ->(s) { { type: :static, value: s.freeze } }
|
94
|
+
}.freeze
|
95
|
+
|
96
|
+
# Regex for unreserved characters (RFC 3986 subset).
|
97
|
+
#
|
98
|
+
# @return [Regexp]
|
99
|
+
UNRESERVED_RE = /\A[a-zA-Z0-9\-._~]+\z/
|
100
|
+
|
101
|
+
# Maximum size of the query parameter cache.
|
102
|
+
#
|
103
|
+
# This constant defines the maximum number of query strings that can be
|
104
|
+
# cached for fast lookup. Once the cache reaches this size, the least
|
105
|
+
# recently used entries will be evicted.
|
106
|
+
#
|
107
|
+
# @return [Integer]
|
108
|
+
QUERY_CACHE_SIZE = 128
|
109
|
+
|
110
|
+
# HTTP method constants.
|
111
|
+
HTTP_GET = 'GET'
|
112
|
+
HTTP_POST = 'POST'
|
113
|
+
HTTP_PUT = 'PUT'
|
114
|
+
HTTP_PATCH = 'PATCH'
|
115
|
+
HTTP_DELETE = 'DELETE'
|
116
|
+
HTTP_HEAD = 'HEAD'
|
117
|
+
HTTP_OPTIONS = 'OPTIONS'
|
118
|
+
|
119
|
+
# Empty constants for reuse.
|
120
|
+
EMPTY_ARRAY = [].freeze
|
121
|
+
EMPTY_PAIR = [EMPTY_ARRAY, EMPTY_ARRAY].freeze
|
122
|
+
EMPTY_STRING = ''.freeze
|
123
|
+
EMPTY_HASH = {}.freeze
|
124
|
+
|
125
|
+
# Maximum number of distinct (method, path) composite keys retained
|
126
|
+
# before the oldest are overwritten in ring order.
|
127
|
+
#
|
128
|
+
# @return [Integer]
|
129
|
+
REQUEST_KEY_CAPACITY = 4096
|
130
|
+
|
131
|
+
# Supported DSL methods for route recording.
|
132
|
+
#
|
133
|
+
# @return [Array<Symbol>]
|
134
|
+
RECORDED_METHODS = %i[
|
135
|
+
get post put patch delete match root
|
136
|
+
resources resource
|
137
|
+
namespace scope constraints defaults
|
138
|
+
mount concern concerns
|
139
|
+
].freeze
|
140
|
+
|
141
|
+
# All supported HTTP verbs.
|
142
|
+
#
|
143
|
+
# @return [Array<Symbol>]
|
144
|
+
VERBS_ALL = %i[get post put patch delete head options].freeze
|
145
|
+
|
146
|
+
# Default result for no traversal match.
|
147
|
+
#
|
148
|
+
# @return [Array]
|
149
|
+
NO_TRAVERSAL_RESULT = [nil, false, {}].freeze
|
150
|
+
|
151
|
+
# Built-in validators for constraints.
|
152
|
+
#
|
153
|
+
# @return [Hash{Symbol => Symbol}]
|
154
|
+
BUILTIN_VALIDATORS = {
|
155
|
+
int: :validate_int_constraint,
|
156
|
+
uuid: :validate_uuid_constraint,
|
157
|
+
email: :validate_email_constraint,
|
158
|
+
slug: :validate_slug_constraint,
|
159
|
+
alpha: :validate_alpha_constraint,
|
160
|
+
alphanumeric: :validate_alphanumeric_constraint
|
49
161
|
}.freeze
|
50
162
|
|
163
|
+
# Build a descriptor Hash for a raw segment string.
|
164
|
+
#
|
165
|
+
# @param raw [String, #to_s] The raw segment string.
|
166
|
+
# @return [Hash] A descriptor hash with frozen values.
|
167
|
+
#
|
168
|
+
# @example
|
169
|
+
# Constant.segment_descriptor(":id") # => { type: :param, name: "id" }
|
51
170
|
def self.segment_descriptor(raw)
|
52
|
-
|
53
|
-
|
54
|
-
factory
|
55
|
-
factory.call(
|
171
|
+
segment_string = raw.to_s
|
172
|
+
dispatch_key = segment_string.empty? ? :default : segment_string.getbyte(0)
|
173
|
+
factory = DESCRIPTOR_FACTORIES[dispatch_key] || DESCRIPTOR_FACTORIES[:default]
|
174
|
+
factory.call(segment_string)
|
56
175
|
end
|
57
176
|
end
|
58
177
|
end
|
@@ -1,12 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module RubyRoutes
|
2
4
|
module LruStrategies
|
5
|
+
# HitStrategy
|
6
|
+
#
|
7
|
+
# Strategy object invoked when a lookup in SmallLru succeeds.
|
8
|
+
# Responsibilities:
|
9
|
+
# - Increment the hit counter.
|
10
|
+
# - Reinsert the accessed key at the logical MRU position to
|
11
|
+
# approximate LRU behavior using simple Hash order.
|
12
|
+
#
|
13
|
+
# Isolation of this logic allows the hot path in SmallLru#get
|
14
|
+
# to delegate without conditionals, and lets alternative
|
15
|
+
# eviction / promotion policies be swapped in tests or future
|
16
|
+
# tuning without rewriting cache code.
|
17
|
+
#
|
18
|
+
# @example (internal usage)
|
19
|
+
# lru = SmallLru.new
|
20
|
+
# RubyRoutes::LruStrategies::HitStrategy.new.call(lru, :k)
|
21
|
+
#
|
22
|
+
# @api internal
|
3
23
|
class HitStrategy
|
24
|
+
# Promote a key on cache hit.
|
25
|
+
#
|
26
|
+
# @param lru [SmallLru] the owning LRU cache
|
27
|
+
# @param key [Object] the key that was found
|
28
|
+
# @return [Object] the cached value
|
4
29
|
def call(lru, key)
|
5
30
|
lru.increment_hits
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
10
37
|
end
|
11
38
|
end
|
12
39
|
end
|
@@ -1,6 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module RubyRoutes
|
2
4
|
module LruStrategies
|
5
|
+
# MissStrategy
|
6
|
+
#
|
7
|
+
# Implements the behavior executed when a key lookup in SmallLru
|
8
|
+
# does not exist. It increments miss counters and returns nil.
|
9
|
+
#
|
10
|
+
# This object-oriented strategy form allows swapping behaviors
|
11
|
+
# without adding conditionals in the hot LRU path.
|
12
|
+
#
|
13
|
+
# @example Basic usage (internal)
|
14
|
+
# lru = SmallLru.new
|
15
|
+
# strategy = RubyRoutes::LruStrategies::MissStrategy.new
|
16
|
+
# strategy.call(lru, :unknown) # => nil (and increments lru.misses)
|
17
|
+
#
|
18
|
+
# @api internal
|
3
19
|
class MissStrategy
|
20
|
+
# Execute miss handling.
|
21
|
+
#
|
22
|
+
# @param lru [SmallLru] the LRU cache instance
|
23
|
+
# @param _key [Object] the missed key (unused)
|
24
|
+
# @return [nil] always nil to signal absence
|
4
25
|
def call(lru, _key)
|
5
26
|
lru.increment_misses
|
6
27
|
nil
|
data/lib/ruby_routes/node.rb
CHANGED
@@ -1,64 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'segment'
|
4
|
+
require_relative 'utility/path_utility'
|
5
|
+
require_relative 'utility/method_utility'
|
2
6
|
|
3
7
|
module RubyRoutes
|
4
|
-
# Node
|
5
|
-
#
|
6
|
-
#
|
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
|
7
27
|
class Node
|
8
28
|
attr_accessor :param_name, :is_endpoint, :dynamic_child, :wildcard_child
|
9
29
|
attr_reader :handlers, :static_children
|
10
30
|
|
31
|
+
include RubyRoutes::Utility::PathUtility
|
32
|
+
include RubyRoutes::Utility::MethodUtility
|
33
|
+
|
11
34
|
def initialize
|
12
|
-
@is_endpoint
|
13
|
-
@handlers
|
35
|
+
@is_endpoint = false
|
36
|
+
@handlers = {}
|
14
37
|
@static_children = {}
|
15
|
-
@dynamic_child
|
16
|
-
@wildcard_child
|
17
|
-
@param_name
|
38
|
+
@dynamic_child = nil
|
39
|
+
@wildcard_child = nil
|
40
|
+
@param_name = nil
|
18
41
|
end
|
19
42
|
|
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
|
20
48
|
def add_handler(method, handler)
|
21
|
-
|
22
|
-
@handlers[
|
49
|
+
method_key = normalize_http_method(method)
|
50
|
+
@handlers[method_key] = handler
|
23
51
|
@is_endpoint = true
|
52
|
+
handler
|
24
53
|
end
|
25
54
|
|
55
|
+
# Fetch a handler for a method.
|
56
|
+
#
|
57
|
+
# @param method [String, Symbol]
|
58
|
+
# @return [Object, nil]
|
26
59
|
def get_handler(method)
|
27
|
-
@handlers[method]
|
60
|
+
@handlers[normalize_http_method(method)]
|
28
61
|
end
|
29
62
|
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
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
|
34
73
|
def traverse_for(segment, index, segments, params)
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
# Capture remaining path segments for wildcard parameter
|
46
|
-
if params && @wildcard_child.param_name
|
47
|
-
remaining = segments[index..-1]
|
48
|
-
params[@wildcard_child.param_name] = remaining.join('/')
|
49
|
-
end
|
50
|
-
return [@wildcard_child, true] # true signals to stop traversal
|
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]
|
51
84
|
end
|
52
85
|
|
53
|
-
|
54
|
-
[nil, false]
|
86
|
+
RubyRoutes::Constant::NO_TRAVERSAL_RESULT
|
55
87
|
end
|
56
88
|
|
57
89
|
private
|
58
90
|
|
59
|
-
#
|
60
|
-
|
61
|
-
|
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('/') }
|
62
112
|
end
|
63
113
|
end
|
64
114
|
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../constant'
|
4
|
+
|
5
|
+
module RubyRoutes
|
6
|
+
class RadixTree
|
7
|
+
# Finder module for traversing the RadixTree and finding routes.
|
8
|
+
# Handles path normalization, segment traversal, and parameter extraction.
|
9
|
+
module Finder
|
10
|
+
|
11
|
+
# Evaluate constraint rules for a candidate route.
|
12
|
+
#
|
13
|
+
# @param route_handler [Object]
|
14
|
+
# @param captured_params [Hash]
|
15
|
+
# @return [Boolean]
|
16
|
+
def check_constraints(route_handler, captured_params)
|
17
|
+
return true unless route_handler.respond_to?(:validate_constraints_fast!)
|
18
|
+
|
19
|
+
begin
|
20
|
+
# Use a duplicate to avoid unintended mutation by validators.
|
21
|
+
route_handler.validate_constraints_fast!(captured_params)
|
22
|
+
true
|
23
|
+
rescue RubyRoutes::ConstraintViolation
|
24
|
+
false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Finds a route handler for the given path and HTTP method.
|
31
|
+
#
|
32
|
+
# @param path_input [String] the input path to match
|
33
|
+
# @param method_input [String, Symbol] the HTTP method
|
34
|
+
# @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
|
40
|
+
|
41
|
+
segments = split_path_cached(path)
|
42
|
+
return [nil, params_out || {}] if segments.empty?
|
43
|
+
|
44
|
+
params = params_out || {}
|
45
|
+
state = traversal_state
|
46
|
+
captured_params = {}
|
47
|
+
|
48
|
+
result = perform_traversal(segments, state, method, params, captured_params)
|
49
|
+
return result unless result.nil?
|
50
|
+
|
51
|
+
finalize_success(state, method, params, captured_params)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Initializes the traversal state for route matching.
|
55
|
+
#
|
56
|
+
# @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
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
# Performs traversal through path segments to find a matching route.
|
68
|
+
#
|
69
|
+
# @param segments [Array<String>] path segments
|
70
|
+
# @param state [Hash] traversal state
|
71
|
+
# @param method [String] normalized HTTP method
|
72
|
+
# @param params [Hash] parameters hash
|
73
|
+
# @param captured_params [Hash] hash to collect captured parameters
|
74
|
+
# @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
|
86
|
+
end
|
87
|
+
|
88
|
+
# Traverses to the next node for a given segment.
|
89
|
+
#
|
90
|
+
# @param node [Node] current node
|
91
|
+
# @param segment [String] current segment
|
92
|
+
# @param index [Integer] segment index
|
93
|
+
# @param segments [Array<String>] all segments
|
94
|
+
# @param params [Hash] parameters hash
|
95
|
+
# @param captured_params [Hash] hash to collect captured parameters
|
96
|
+
# @return [Array] [next_node, stop_traversal, segment_captured]
|
97
|
+
def traverse_for_segment(node, segment, index, segments, params, captured_params)
|
98
|
+
next_node, stop, segment_captured = node.traverse_for(segment, index, segments, params)
|
99
|
+
captured_params.merge!(segment_captured) if segment_captured
|
100
|
+
[next_node, stop]
|
101
|
+
end
|
102
|
+
|
103
|
+
# Records the current node as a candidate match.
|
104
|
+
#
|
105
|
+
# @param state [Hash] traversal state
|
106
|
+
# @param _method [String] HTTP method (unused)
|
107
|
+
# @param params [Hash] parameters hash
|
108
|
+
# @param captured_params [Hash] captured parameters from traversal
|
109
|
+
def record_candidate(state, _method, params, captured_params)
|
110
|
+
state[:best_node] = state[:current]
|
111
|
+
state[:best_params] = params.dup
|
112
|
+
state[:best_captured] = captured_params.dup
|
113
|
+
end
|
114
|
+
|
115
|
+
# Checks if the node is an endpoint with a handler for the method.
|
116
|
+
#
|
117
|
+
# @param node [Node] the node to check
|
118
|
+
# @param method [String] HTTP method
|
119
|
+
# @return [Boolean] true if endpoint and handler exists
|
120
|
+
def endpoint_with_method?(node, method)
|
121
|
+
node.is_endpoint && node.handlers[method]
|
122
|
+
end
|
123
|
+
|
124
|
+
# Finalizes the result when traversal fails mid-path.
|
125
|
+
#
|
126
|
+
# @param state [Hash] traversal state
|
127
|
+
# @param method [String] HTTP method
|
128
|
+
# @param params [Hash] parameters hash
|
129
|
+
# @param captured_params [Hash] captured parameters from traversal
|
130
|
+
# @return [Array] [handler, params] or [nil, params]
|
131
|
+
def finalize_on_fail(state, method, params, captured_params)
|
132
|
+
best_params = state[:best_params] || params
|
133
|
+
best_captured = state[:best_captured] || captured_params
|
134
|
+
finalize_match(state[:best_node], method, best_params, best_captured)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Finalizes the result after successful traversal.
|
138
|
+
#
|
139
|
+
# @param state [Hash] traversal state
|
140
|
+
# @param method [String] HTTP method
|
141
|
+
# @param params [Hash] parameters hash
|
142
|
+
# @param captured_params [Hash] captured parameters from traversal
|
143
|
+
# @return [Array] [handler, params] or [nil, params]
|
144
|
+
def finalize_success(state, method, params, captured_params)
|
145
|
+
result = finalize_match(state[:current], method, params, captured_params)
|
146
|
+
return result if result[0]
|
147
|
+
|
148
|
+
# Try best candidate if current failed
|
149
|
+
if state[:best_node]
|
150
|
+
best_params = state[:best_params] || params
|
151
|
+
best_captured = state[:best_captured] || captured_params
|
152
|
+
finalize_match(state[:best_node], method, best_params, best_captured)
|
153
|
+
else
|
154
|
+
result
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Falls back to the best candidate if no exact match.
|
159
|
+
#
|
160
|
+
# @param state [Hash] traversal state
|
161
|
+
# @param method [String] HTTP method
|
162
|
+
# @param params [Hash] parameters hash
|
163
|
+
# @param captured_params [Hash] captured parameters from traversal
|
164
|
+
# @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
|
168
|
+
|
169
|
+
# Common method to finalize a match attempt.
|
170
|
+
# Assumes the node is already validated as an endpoint.
|
171
|
+
#
|
172
|
+
# @param node [Node] the node to check for a handler
|
173
|
+
# @param method [String] HTTP method
|
174
|
+
# @param params [Hash] parameters hash
|
175
|
+
# @param captured_params [Hash] captured parameters from traversal
|
176
|
+
# @return [Array] [handler, params] or [nil, params]
|
177
|
+
def finalize_match(node, method, params, captured_params)
|
178
|
+
if node && endpoint_with_method?(node, method)
|
179
|
+
handler = node.handlers[method]
|
180
|
+
# Apply captured params before constraint validation
|
181
|
+
apply_captured_params(params, captured_params)
|
182
|
+
if check_constraints(handler, params)
|
183
|
+
return [handler, params]
|
184
|
+
end
|
185
|
+
end
|
186
|
+
# For non-matching paths, return nil
|
187
|
+
apply_captured_params(params, captured_params)
|
188
|
+
[nil, params]
|
189
|
+
end
|
190
|
+
|
191
|
+
# Handles matching for the root path.
|
192
|
+
#
|
193
|
+
# @param method [String] HTTP method
|
194
|
+
# @param params_out [Hash] parameters hash
|
195
|
+
# @return [Array] [handler, params] or [nil, params]
|
196
|
+
def root_match(method, params_out)
|
197
|
+
if @root.is_endpoint && (handler = @root.handlers[method])
|
198
|
+
[handler, params_out || {}]
|
199
|
+
else
|
200
|
+
[nil, params_out || {}]
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Applies captured parameters to the final params hash.
|
205
|
+
#
|
206
|
+
# @param params [Hash] the final parameters hash
|
207
|
+
# @param captured_params [Hash] captured parameters from traversal
|
208
|
+
def apply_captured_params(params, captured_params)
|
209
|
+
params.merge!(captured_params) if captured_params && !captured_params.empty?
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|