ruby_routes 2.2.0 → 2.3.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 +232 -162
- 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 +75 -33
- data/lib/ruby_routes/radix_tree/finder.rb +164 -0
- data/lib/ruby_routes/radix_tree/inserter.rb +98 -0
- data/lib/ruby_routes/radix_tree.rb +79 -227
- data/lib/ruby_routes/route/check_helpers.rb +109 -0
- data/lib/ruby_routes/route/constraint_validator.rb +159 -0
- data/lib/ruby_routes/route/param_support.rb +202 -0
- data/lib/ruby_routes/route/path_builder.rb +86 -0
- data/lib/ruby_routes/route/path_generation.rb +102 -0
- data/lib/ruby_routes/route/query_helpers.rb +56 -0
- data/lib/ruby_routes/route/segment_compiler.rb +163 -0
- data/lib/ruby_routes/route/small_lru.rb +93 -18
- data/lib/ruby_routes/route/validation_helpers.rb +151 -0
- data/lib/ruby_routes/route/warning_helpers.rb +54 -0
- data/lib/ruby_routes/route.rb +124 -501
- data/lib/ruby_routes/route_set/cache_helpers.rb +174 -0
- data/lib/ruby_routes/route_set/collection_helpers.rb +127 -0
- data/lib/ruby_routes/route_set.rb +120 -133
- data/lib/ruby_routes/router/build_helpers.rb +100 -0
- data/lib/ruby_routes/router/builder.rb +96 -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 +109 -0
- data/lib/ruby_routes/router.rb +193 -181
- 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 +56 -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 +161 -84
- 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 = '/'
|
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 = ''
|
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,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'segment'
|
4
|
+
require_relative 'utility/path_utility'
|
2
5
|
|
3
6
|
module RubyRoutes
|
4
|
-
# Node
|
5
|
-
#
|
6
|
-
#
|
7
|
+
# Node
|
8
|
+
#
|
9
|
+
# A single vertex in the routing radix tree.
|
10
|
+
#
|
11
|
+
# Structure:
|
12
|
+
# - static_children: Hash<String, Node> exact literal matches.
|
13
|
+
# - dynamic_child: Node (":param") matches any single segment, captures value.
|
14
|
+
# - wildcard_child: Node ("*splat") matches remaining segments (greedy).
|
15
|
+
#
|
16
|
+
# Handlers:
|
17
|
+
# - @handlers maps canonical HTTP method strings (e.g. "GET") to Route objects (or callable handlers).
|
18
|
+
# - @is_endpoint marks that at least one handler is attached (terminal path).
|
19
|
+
#
|
20
|
+
# Matching precedence (most → least specific):
|
21
|
+
# static → dynamic → wildcard
|
22
|
+
#
|
23
|
+
# Thread safety: not thread-safe (build during boot).
|
24
|
+
#
|
25
|
+
# @api internal
|
7
26
|
class Node
|
8
27
|
attr_accessor :param_name, :is_endpoint, :dynamic_child, :wildcard_child
|
9
28
|
attr_reader :handlers, :static_children
|
10
29
|
|
30
|
+
include RubyRoutes::Utility::PathUtility
|
31
|
+
|
11
32
|
def initialize
|
12
|
-
@is_endpoint
|
13
|
-
@handlers
|
33
|
+
@is_endpoint = false
|
34
|
+
@handlers = {}
|
14
35
|
@static_children = {}
|
15
|
-
@dynamic_child
|
16
|
-
@wildcard_child
|
17
|
-
@param_name
|
36
|
+
@dynamic_child = nil
|
37
|
+
@wildcard_child = nil
|
38
|
+
@param_name = nil
|
18
39
|
end
|
19
40
|
|
41
|
+
# Register a handler under an HTTP method.
|
42
|
+
#
|
43
|
+
# @param method [String, Symbol]
|
44
|
+
# @param handler [Object] route or callable
|
45
|
+
# @return [Object] handler
|
20
46
|
def add_handler(method, handler)
|
21
47
|
method_str = normalize_method(method)
|
22
48
|
@handlers[method_str] = handler
|
23
49
|
@is_endpoint = true
|
50
|
+
handler
|
24
51
|
end
|
25
52
|
|
53
|
+
# Fetch a handler for a method.
|
54
|
+
#
|
55
|
+
# @param method [String, Symbol]
|
56
|
+
# @return [Object, nil]
|
26
57
|
def get_handler(method)
|
27
|
-
@handlers[method]
|
58
|
+
@handlers[normalize_method(method)]
|
28
59
|
end
|
29
60
|
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
61
|
+
# Traverses from this node using a single path segment.
|
62
|
+
# Returns [next_node_or_nil, stop_traversal(Boolean)].
|
63
|
+
#
|
64
|
+
# Optimized + simplified (cyclomatic / perceived complexity, length).
|
34
65
|
def traverse_for(segment, index, segments, params)
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
elsif @dynamic_child
|
40
|
-
# Capture parameter if params hash provided and param_name is set
|
41
|
-
params[@dynamic_child.param_name] = segment if params && @dynamic_child.param_name
|
66
|
+
return [@static_children[segment], false] if @static_children[segment]
|
67
|
+
|
68
|
+
if @dynamic_child
|
69
|
+
capture_dynamic_param(params, @dynamic_child, segment)
|
42
70
|
return [@dynamic_child, false]
|
43
|
-
# Try wildcard child (consumes remaining segments) - least specific
|
44
|
-
elsif @wildcard_child
|
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
|
51
71
|
end
|
52
72
|
|
53
|
-
|
54
|
-
|
73
|
+
if @wildcard_child
|
74
|
+
capture_wildcard_param(params, @wildcard_child, segments, index)
|
75
|
+
return [@wildcard_child, true]
|
76
|
+
end
|
77
|
+
|
78
|
+
RubyRoutes::Constant::NO_TRAVERSAL_RESULT
|
55
79
|
end
|
56
80
|
|
57
81
|
private
|
58
82
|
|
59
|
-
#
|
60
|
-
|
61
|
-
|
83
|
+
# Captures a dynamic parameter value into the params hash if applicable.
|
84
|
+
#
|
85
|
+
# @param params [Hash, nil] the parameters hash to update
|
86
|
+
# @param dyn_node [Node] the dynamic child node
|
87
|
+
# @param value [String] the segment value to capture
|
88
|
+
def capture_dynamic_param(params, dyn_node, value)
|
89
|
+
return unless params && dyn_node.param_name
|
90
|
+
|
91
|
+
params[dyn_node.param_name] = value
|
92
|
+
end
|
93
|
+
|
94
|
+
# Captures a wildcard parameter value into the params hash if applicable.
|
95
|
+
#
|
96
|
+
# @param params [Hash, nil] the parameters hash to update
|
97
|
+
# @param wc_node [Node] the wildcard child node
|
98
|
+
# @param segments [Array<String>] the full path segments
|
99
|
+
# @param index [Integer] the current segment index
|
100
|
+
def capture_wildcard_param(params, wc_node, segments, index)
|
101
|
+
return unless params && wc_node.param_name
|
102
|
+
|
103
|
+
params[wc_node.param_name] = segments[index..].join('/')
|
62
104
|
end
|
63
105
|
end
|
64
106
|
end
|
@@ -0,0 +1,164 @@
|
|
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 matching routes.
|
8
|
+
# Handles path normalization, segment traversal, and parameter extraction.
|
9
|
+
#
|
10
|
+
# @module RubyRoutes::RadixTree::Finder
|
11
|
+
module Finder
|
12
|
+
private
|
13
|
+
|
14
|
+
# Finds a route handler for the given path and HTTP method.
|
15
|
+
#
|
16
|
+
# @param path_input [String] the input path to match
|
17
|
+
# @param method_input [String, Symbol] the HTTP method
|
18
|
+
# @param params_out [Hash] optional output hash for captured parameters
|
19
|
+
# @return [Array] [handler, params] or [nil, params] if no match
|
20
|
+
def find(path_input, method_input, params_out = {})
|
21
|
+
path = path_input.to_s
|
22
|
+
method = normalize_http_method(method_input)
|
23
|
+
return root_match(method, params_out) if path.empty? || path == RubyRoutes::Constant::ROOT_PATH
|
24
|
+
|
25
|
+
segments = split_path_cached(path)
|
26
|
+
return [nil, params_out || {}] if segments.empty?
|
27
|
+
|
28
|
+
params = params_out || {}
|
29
|
+
state = traversal_state
|
30
|
+
|
31
|
+
perform_traversal(segments, state, method, params)
|
32
|
+
|
33
|
+
finalize_success(state, method, params)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Initializes the traversal state for route matching.
|
37
|
+
#
|
38
|
+
# @return [Hash] state hash with :current, :best_node, :best_params, :matched
|
39
|
+
def traversal_state
|
40
|
+
{
|
41
|
+
current: @root_node,
|
42
|
+
best_node: nil,
|
43
|
+
best_params: nil,
|
44
|
+
matched: false # Track if any segment was successfully matched
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
# Performs traversal through path segments to find a matching route.
|
49
|
+
#
|
50
|
+
# @param segments [Array<String>] path segments
|
51
|
+
# @param state [Hash] traversal state
|
52
|
+
# @param method [String] normalized HTTP method
|
53
|
+
# @param params [Hash] parameters hash
|
54
|
+
def perform_traversal(segments, state, method, params)
|
55
|
+
segments.each_with_index do |segment, index|
|
56
|
+
next_node, stop = traverse_for_segment(state[:current], segment, index, segments, params)
|
57
|
+
return finalize_on_fail(state, method, params) unless next_node
|
58
|
+
|
59
|
+
state[:current] = next_node
|
60
|
+
state[:matched] = true # Set matched to true if at least one segment matched
|
61
|
+
record_candidate(state, method, params) if endpoint_with_method?(state[:current], method)
|
62
|
+
break if stop
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Traverses to the next node for a given segment.
|
67
|
+
#
|
68
|
+
# @param node [Node] current node
|
69
|
+
# @param segment [String] current segment
|
70
|
+
# @param index [Integer] segment index
|
71
|
+
# @param segments [Array<String>] all segments
|
72
|
+
# @param params [Hash] parameters hash
|
73
|
+
# @return [Array] [next_node, stop_traversal]
|
74
|
+
def traverse_for_segment(node, segment, index, segments, params)
|
75
|
+
node.traverse_for(segment, index, segments, params)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Records the current node as a candidate match.
|
79
|
+
#
|
80
|
+
# @param state [Hash] traversal state
|
81
|
+
# @param _method [String] HTTP method (unused)
|
82
|
+
# @param params [Hash] parameters hash
|
83
|
+
def record_candidate(state, _method, params)
|
84
|
+
state[:best_node] = state[:current]
|
85
|
+
state[:best_params] = params.dup
|
86
|
+
end
|
87
|
+
|
88
|
+
# Checks if the node is an endpoint with a handler for the method.
|
89
|
+
#
|
90
|
+
# @param node [Node] the node to check
|
91
|
+
# @param method [String] HTTP method
|
92
|
+
# @return [Boolean] true if endpoint and handler exists
|
93
|
+
def endpoint_with_method?(node, method)
|
94
|
+
node.is_endpoint && node.handlers[method]
|
95
|
+
end
|
96
|
+
|
97
|
+
# Finalizes the result when traversal fails mid-path.
|
98
|
+
#
|
99
|
+
# @param state [Hash] traversal state
|
100
|
+
# @param method [String] HTTP method
|
101
|
+
# @param params [Hash] parameters hash
|
102
|
+
# @return [Array] [handler, params] or [nil, params]
|
103
|
+
def finalize_on_fail(state, method, params)
|
104
|
+
if state[:best_node]
|
105
|
+
handler = state[:best_node].handlers[method]
|
106
|
+
return constraints_pass?(handler, state[:best_params]) ? [handler, state[:best_params]] : [nil, params]
|
107
|
+
end
|
108
|
+
[nil, params]
|
109
|
+
end
|
110
|
+
|
111
|
+
# Finalizes the result after successful traversal.
|
112
|
+
#
|
113
|
+
# @param state [Hash] traversal state
|
114
|
+
# @param method [String] HTTP method
|
115
|
+
# @param params [Hash] parameters hash
|
116
|
+
# @return [Array] [handler, params] or [nil, params]
|
117
|
+
def finalize_success(state, method, params)
|
118
|
+
node = state[:current]
|
119
|
+
if endpoint_with_method?(node, method) && state[:matched]
|
120
|
+
handler = node.handlers[method]
|
121
|
+
return [handler, params] if constraints_pass?(handler, params)
|
122
|
+
end
|
123
|
+
# For non-matching paths, return nil
|
124
|
+
[nil, params]
|
125
|
+
end
|
126
|
+
|
127
|
+
# Falls back to the best candidate if no exact match.
|
128
|
+
#
|
129
|
+
# @param state [Hash] traversal state
|
130
|
+
# @param method [String] HTTP method
|
131
|
+
# @param params [Hash] parameters hash
|
132
|
+
# @return [Array] [handler, params] or [nil, params]
|
133
|
+
def fallback_candidate(state, method, params)
|
134
|
+
if state[:best_node] && state[:best_node] != @root_node
|
135
|
+
handler = state[:best_node].handlers[method]
|
136
|
+
return [handler, state[:best_params]] if handler && constraints_pass?(handler, state[:best_params])
|
137
|
+
end
|
138
|
+
[nil, params]
|
139
|
+
end
|
140
|
+
|
141
|
+
# Handles matching for the root path.
|
142
|
+
#
|
143
|
+
# @param method [String] HTTP method
|
144
|
+
# @param params_out [Hash] parameters hash
|
145
|
+
# @return [Array] [handler, params] or [nil, params]
|
146
|
+
def root_match(method, params_out)
|
147
|
+
if @root_node.is_endpoint && (handler = @root_node.handlers[method])
|
148
|
+
[handler, params_out || {}]
|
149
|
+
else
|
150
|
+
[nil, params_out || {}]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Checks if constraints pass for the handler.
|
155
|
+
#
|
156
|
+
# @param handler [Object] the route handler
|
157
|
+
# @param params [Hash] parameters hash
|
158
|
+
# @return [Boolean] true if constraints pass
|
159
|
+
def constraints_pass?(handler, params)
|
160
|
+
check_constraints(handler, params&.dup || {})
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyRoutes
|
4
|
+
class RadixTree
|
5
|
+
# Inserter module for adding routes to the RadixTree.
|
6
|
+
# Handles tokenization, node advancement, and endpoint finalization.
|
7
|
+
#
|
8
|
+
# @module RubyRoutes::RadixTree::Inserter
|
9
|
+
module Inserter
|
10
|
+
private
|
11
|
+
|
12
|
+
# Inserts a route into the RadixTree for the given path and HTTP methods.
|
13
|
+
#
|
14
|
+
# @param path_string [String] the path to insert
|
15
|
+
# @param http_methods [Array<String>] the HTTP methods for the route
|
16
|
+
# @param route_handler [Object] the handler for the route
|
17
|
+
# @return [Object] the route handler
|
18
|
+
def insert_route(path_string, http_methods, route_handler)
|
19
|
+
return route_handler if path_string.nil? || path_string.empty?
|
20
|
+
|
21
|
+
tokens = split_path(path_string)
|
22
|
+
current_node = @root_node
|
23
|
+
tokens.each { |token| current_node = advance_node(current_node, token) }
|
24
|
+
finalize_endpoint(current_node, http_methods, route_handler)
|
25
|
+
route_handler
|
26
|
+
end
|
27
|
+
|
28
|
+
# Advances to the next node based on the token type.
|
29
|
+
#
|
30
|
+
# @param current_node [Node] the current node in the tree
|
31
|
+
# @param token [String] the token to process
|
32
|
+
# @return [Node] the next node
|
33
|
+
def advance_node(current_node, token)
|
34
|
+
case token[0]
|
35
|
+
when ':'
|
36
|
+
handle_dynamic(current_node, token)
|
37
|
+
when '*'
|
38
|
+
handle_wildcard(current_node, token)
|
39
|
+
else
|
40
|
+
handle_static(current_node, token)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Handles dynamic parameter tokens (e.g., :id).
|
45
|
+
#
|
46
|
+
# @param current_node [Node] the current node
|
47
|
+
# @param token [String] the dynamic token
|
48
|
+
# @return [Node] the dynamic child node
|
49
|
+
def handle_dynamic(current_node, token)
|
50
|
+
param_name = token[1..]
|
51
|
+
current_node.dynamic_child ||= build_param_node(param_name)
|
52
|
+
current_node.dynamic_child
|
53
|
+
end
|
54
|
+
|
55
|
+
# Handles wildcard tokens (e.g., *splat).
|
56
|
+
#
|
57
|
+
# @param current_node [Node] the current node
|
58
|
+
# @param token [String] the wildcard token
|
59
|
+
# @return [Node] the wildcard child node
|
60
|
+
def handle_wildcard(current_node, token)
|
61
|
+
param_name = token[1..]
|
62
|
+
param_name = 'splat' if param_name.nil? || param_name.empty?
|
63
|
+
current_node.wildcard_child ||= build_param_node(param_name)
|
64
|
+
current_node.wildcard_child
|
65
|
+
end
|
66
|
+
|
67
|
+
# Handles static literal tokens.
|
68
|
+
#
|
69
|
+
# @param current_node [Node] the current node
|
70
|
+
# @param token [String] the static token
|
71
|
+
# @return [Node] the static child node
|
72
|
+
def handle_static(current_node, token)
|
73
|
+
literal_token = token.freeze
|
74
|
+
current_node.static_children[literal_token] ||= Node.new
|
75
|
+
current_node.static_children[literal_token]
|
76
|
+
end
|
77
|
+
|
78
|
+
# Builds a new node for parameter capture.
|
79
|
+
#
|
80
|
+
# @param param_name [String] the parameter name
|
81
|
+
# @return [Node] the new parameter node
|
82
|
+
def build_param_node(param_name)
|
83
|
+
node = Node.new
|
84
|
+
node.param_name = param_name
|
85
|
+
node
|
86
|
+
end
|
87
|
+
|
88
|
+
# Finalizes the endpoint by adding handlers for HTTP methods.
|
89
|
+
#
|
90
|
+
# @param node [Node] the endpoint node
|
91
|
+
# @param http_methods [Array<String>] the HTTP methods
|
92
|
+
# @param route_handler [Object] the route handler
|
93
|
+
def finalize_endpoint(node, http_methods, route_handler)
|
94
|
+
http_methods.each { |http_method| node.add_handler(http_method, route_handler) }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|