ruby_routes 2.1.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +232 -162
  3. data/lib/ruby_routes/constant.rb +137 -18
  4. data/lib/ruby_routes/lru_strategies/hit_strategy.rb +31 -4
  5. data/lib/ruby_routes/lru_strategies/miss_strategy.rb +21 -0
  6. data/lib/ruby_routes/node.rb +82 -41
  7. data/lib/ruby_routes/radix_tree/finder.rb +164 -0
  8. data/lib/ruby_routes/radix_tree/inserter.rb +98 -0
  9. data/lib/ruby_routes/radix_tree.rb +83 -142
  10. data/lib/ruby_routes/route/check_helpers.rb +109 -0
  11. data/lib/ruby_routes/route/constraint_validator.rb +159 -0
  12. data/lib/ruby_routes/route/param_support.rb +202 -0
  13. data/lib/ruby_routes/route/path_builder.rb +86 -0
  14. data/lib/ruby_routes/route/path_generation.rb +102 -0
  15. data/lib/ruby_routes/route/query_helpers.rb +56 -0
  16. data/lib/ruby_routes/route/segment_compiler.rb +163 -0
  17. data/lib/ruby_routes/route/small_lru.rb +96 -17
  18. data/lib/ruby_routes/route/validation_helpers.rb +151 -0
  19. data/lib/ruby_routes/route/warning_helpers.rb +54 -0
  20. data/lib/ruby_routes/route.rb +121 -451
  21. data/lib/ruby_routes/route_set/cache_helpers.rb +174 -0
  22. data/lib/ruby_routes/route_set/collection_helpers.rb +127 -0
  23. data/lib/ruby_routes/route_set.rb +126 -148
  24. data/lib/ruby_routes/router/build_helpers.rb +100 -0
  25. data/lib/ruby_routes/router/builder.rb +96 -0
  26. data/lib/ruby_routes/router/http_helpers.rb +135 -0
  27. data/lib/ruby_routes/router/resource_helpers.rb +137 -0
  28. data/lib/ruby_routes/router/scope_helpers.rb +109 -0
  29. data/lib/ruby_routes/router.rb +196 -179
  30. data/lib/ruby_routes/segment.rb +28 -8
  31. data/lib/ruby_routes/segments/base_segment.rb +40 -4
  32. data/lib/ruby_routes/segments/dynamic_segment.rb +48 -12
  33. data/lib/ruby_routes/segments/static_segment.rb +43 -7
  34. data/lib/ruby_routes/segments/wildcard_segment.rb +56 -12
  35. data/lib/ruby_routes/string_extensions.rb +52 -15
  36. data/lib/ruby_routes/url_helpers.rb +106 -24
  37. data/lib/ruby_routes/utility/inflector_utility.rb +35 -0
  38. data/lib/ruby_routes/utility/key_builder_utility.rb +179 -0
  39. data/lib/ruby_routes/utility/method_utility.rb +137 -0
  40. data/lib/ruby_routes/utility/path_utility.rb +89 -0
  41. data/lib/ruby_routes/utility/route_utility.rb +49 -0
  42. data/lib/ruby_routes/version.rb +3 -1
  43. data/lib/ruby_routes.rb +68 -11
  44. metadata +30 -7
@@ -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
- nxt = node.dynamic_child
25
- params[nxt.param_name.to_s] = segment if params && nxt.param_name
26
- [nxt, false]
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
- nxt = node.wildcard_child
32
- params[nxt.param_name.to_s] = segments[idx..-1].join('/') if params && nxt.param_name
33
- [nxt, true]
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
- # default returns nil (no match). RadixTree#find will then return [nil, {}].
37
- default: lambda { |_node, _segment, _idx, _segments, _params| nil }
74
+ default: ->(_node, _segment, _idx, _segments, _params) { nil }
38
75
  }.freeze
39
76
 
40
- # singleton instances to avoid per-LRU allocations
41
- LRU_HIT_STRATEGY = RubyRoutes::LruStrategies::HitStrategy.new.freeze
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
- # Descriptor factories for segment classification (O(1) dispatch by first byte).
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 => ->(s) { { type: :splat, name: (s[1..-1] || 'splat').freeze } }, # '*'
47
- 58 => ->(s) { { type: :param, name: s[1..-1].freeze } }, # ':'
48
- :default => ->(s) { { type: :static, value: s.freeze } } # Intern static values
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
- s = raw.to_s
53
- key = s.empty? ? :default : s.getbyte(0)
54
- factory = DESCRIPTOR_FACTORIES[key] || DESCRIPTOR_FACTORIES[:default]
55
- factory.call(s)
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
- h = lru.instance_variable_get(:@h)
7
- val = h.delete(key)
8
- h[key] = val
9
- val
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
@@ -1,65 +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
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
4
26
  class Node
5
- attr_accessor :static_children, :dynamic_child, :wildcard_child,
6
- :handlers, :param_name, :is_endpoint
27
+ attr_accessor :param_name, :is_endpoint, :dynamic_child, :wildcard_child
28
+ attr_reader :handlers, :static_children
29
+
30
+ include RubyRoutes::Utility::PathUtility
7
31
 
8
32
  def initialize
33
+ @is_endpoint = false
34
+ @handlers = {}
9
35
  @static_children = {}
10
- @dynamic_child = nil
11
- @wildcard_child = nil
12
- @handlers = {}
13
- @param_name = nil
14
- @is_endpoint = false
36
+ @dynamic_child = nil
37
+ @wildcard_child = nil
38
+ @param_name = nil
39
+ end
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
46
+ def add_handler(method, handler)
47
+ method_str = normalize_method(method)
48
+ @handlers[method_str] = handler
49
+ @is_endpoint = true
50
+ handler
15
51
  end
16
52
 
17
- # Fast traversal: minimal allocations, streamlined branching
18
- # Returns [next_node_or_nil, should_break_bool] or [nil, false] if no match.
53
+ # Fetch a handler for a method.
54
+ #
55
+ # @param method [String, Symbol]
56
+ # @return [Object, nil]
57
+ def get_handler(method)
58
+ @handlers[normalize_method(method)]
59
+ end
60
+
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).
19
65
  def traverse_for(segment, index, segments, params)
20
- # Static match: O(1) hash lookup
21
- child = @static_children[segment]
22
- return [child, false] if child
66
+ return [@static_children[segment], false] if @static_children[segment]
23
67
 
24
- # Dynamic match: single segment capture
25
- if (dyn = @dynamic_child)
26
- params[dyn.param_name] = segment if params
27
- return [dyn, false]
68
+ if @dynamic_child
69
+ capture_dynamic_param(params, @dynamic_child, segment)
70
+ return [@dynamic_child, false]
28
71
  end
29
72
 
30
- # Wildcard match: consume remainder (last resort)
31
- if (wild = @wildcard_child)
32
- if params
33
- # Build remainder path without intermediate array allocation
34
- remainder = segments[index..-1]
35
- params[wild.param_name] = remainder.size == 1 ? remainder[0] : remainder.join('/')
36
- end
37
- return [wild, true]
73
+ if @wildcard_child
74
+ capture_wildcard_param(params, @wildcard_child, segments, index)
75
+ return [@wildcard_child, true]
38
76
  end
39
77
 
40
- # No match
41
- [nil, false]
78
+ RubyRoutes::Constant::NO_TRAVERSAL_RESULT
42
79
  end
43
80
 
44
- # Pre-cache param names as strings to avoid repeated .to_s calls
45
- def param_name
46
- @param_name_str ||= @param_name&.to_s
47
- end
81
+ private
48
82
 
49
- def param_name=(name)
50
- @param_name = name
51
- @param_name_str = nil # invalidate cache
52
- end
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
53
90
 
54
- # Normalize method once and cache string keys
55
- def add_handler(method, handler)
56
- method_key = method.to_s.upcase
57
- @handlers[method_key] = handler
58
- @is_endpoint = true
91
+ params[dyn_node.param_name] = value
59
92
  end
60
93
 
61
- def get_handler(method)
62
- @handlers[method] # assume already normalized upstream
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('/')
63
104
  end
64
105
  end
65
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