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.
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 +75 -33
  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 +79 -227
  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 +93 -18
  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 +124 -501
  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 +120 -133
  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 +193 -181
  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 +161 -84
  39. data/lib/ruby_routes/utility/method_utility.rb +137 -0
  40. data/lib/ruby_routes/utility/path_utility.rb +75 -28
  41. data/lib/ruby_routes/utility/route_utility.rb +30 -2
  42. data/lib/ruby_routes/version.rb +3 -1
  43. data/lib/ruby_routes.rb +68 -11
  44. metadata +27 -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,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 represents a single node in the radix tree structure.
5
- # Each node can have static children (exact matches), one dynamic child (parameter capture),
6
- # and one wildcard child (consumes remaining path segments).
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 = false
13
- @handlers = {}
33
+ @is_endpoint = false
34
+ @handlers = {}
14
35
  @static_children = {}
15
- @dynamic_child = nil
16
- @wildcard_child = nil
17
- @param_name = nil
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
- # Fast traversal method with minimal allocations and streamlined branching.
31
- # Matching order: static (most specific) → dynamic → wildcard (least specific)
32
- # Returns [next_node_or_nil, should_break_bool] where should_break indicates
33
- # wildcard capture that consumes remaining path segments.
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
- # Try static child first (most specific) - O(1) hash lookup
36
- if @static_children.key?(segment)
37
- return [@static_children[segment], false]
38
- # Try dynamic child (parameter capture) - less specific than static
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
- # No match found at this node
54
- [nil, false]
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
- # Fast method normalization - converts method to uppercase string
60
- def normalize_method(method)
61
- method.to_s.upcase
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