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,205 +1,222 @@
1
- module RubyRoutes
2
- class Router
3
- attr_reader :route_set
1
+ # frozen_string_literal: true
4
2
 
5
- def initialize(&block)
6
- @route_set = RouteSet.new
7
- @scope_stack = []
8
- @concerns = {}
9
- instance_eval(&block) if block_given?
10
- end
11
-
12
- # Basic route definition
13
- def get(path, options = {})
14
- add_route(path, options.merge(via: :get))
15
- end
16
-
17
- def post(path, options = {})
18
- add_route(path, options.merge(via: :post))
19
- end
20
-
21
- def put(path, options = {})
22
- add_route(path, options.merge(via: :put))
23
- end
24
-
25
- def patch(path, options = {})
26
- add_route(path, options.merge(via: :patch))
27
- end
3
+ require_relative 'utility/inflector_utility'
4
+ require_relative 'utility/route_utility'
5
+ require_relative 'router/http_helpers'
28
6
 
29
- def delete(path, options = {})
30
- add_route(path, options.merge(via: :delete))
31
- end
7
+ module RubyRoutes
8
+ # RubyRoutes::Router
9
+ #
10
+ # Public DSL entrypoint for defining application routes.
11
+ #
12
+ # Usage:
13
+ # router = RubyRoutes::Router.new do
14
+ # get '/health', to: 'system#health'
15
+ # resources :users
16
+ # namespace :admin do
17
+ # resources :posts
18
+ # end
19
+ # end
20
+ #
21
+ # Thread Safety:
22
+ # Build routes at boot. Mutating after multiple threads start serving
23
+ # requests is not supported.
24
+ #
25
+ # Responsibilities:
26
+ # - Provide Rails‑inspired DSL (get/post/put/patch/delete/match/root).
27
+ # - Define RESTful collections via `#resources` and singular via `#resource`.
28
+ # - Support scoping (namespace / scope / constraints / defaults).
29
+ # - Allow reusable blocks via concerns (`#concern` / `#concerns`).
30
+ # - Mount external Rack apps (`#mount`).
31
+ # - Delegate route object creation & storage to `RouteSet` / `RouteUtility`.
32
+ #
33
+ # Design Notes:
34
+ # - Scope stack is an array of shallow hashes (path/module/constraints/defaults).
35
+ # - Scopes are applied inner-first (reverse_each). For options (constraints/defaults),
36
+ # inner values should override outer ones.
37
+ # - Options hashes passed by users are duplicated only when necessary
38
+ # (see `build_route_options`) to reduce allocation churn.
39
+ #
40
+ # Public API Surface (Stable):
41
+ # - `#initialize` (block form)
42
+ # - HTTP verb helpers (`get/post/put/patch/delete/match`)
43
+ # - `#root`
44
+ # - `#resources` / `#resource`
45
+ # - `#namespace` / `#scope` / `#constraints` / `#defaults`
46
+ # - `#concern` / `#concerns`
47
+ # - `#mount`
48
+ #
49
+ # Internal / Subject to Change:
50
+ # - `#add_route`
51
+ # - `#apply_scope`
52
+ # - `#build_route_options`
53
+ # - `#push_scope`
54
+ #
55
+ # @api public
56
+ class Router
57
+ VERBS_ALL = RubyRoutes::Constant::VERBS_ALL
32
58
 
33
- def match(path, options = {})
34
- add_route(path, options)
35
- end
59
+ attr_reader :route_set
36
60
 
37
- # Resources routing (Rails-like)
38
- def resources(name, options = {}, &block)
39
- singular = name.to_s.singularize
40
- plural = (options[:path] || name.to_s.pluralize)
41
- controller = options[:controller] || plural
42
-
43
- # Collection routes
44
- get "/#{plural}", options.merge(to: "#{controller}#index")
45
- get "/#{plural}/new", options.merge(to: "#{controller}#new")
46
- post "/#{plural}", options.merge(to: "#{controller}#create")
47
-
48
- # Member routes
49
- get "/#{plural}/:id", options.merge(to: "#{controller}#show")
50
- get "/#{plural}/:id/edit", options.merge(to: "#{controller}#edit")
51
- put "/#{plural}/:id", options.merge(to: "#{controller}#update")
52
- patch "/#{plural}/:id", options.merge(to: "#{controller}#update")
53
- delete "/#{plural}/:id", options.merge(to: "#{controller}#destroy")
54
-
55
- # Nested resources if specified
56
- if options[:nested]
57
- nested_name = options[:nested]
58
- nested_singular = nested_name.to_s.singularize
59
- nested_plural = nested_name.to_s.pluralize
60
-
61
- get "/#{plural}/:id/#{nested_plural}", options.merge(to: "#{nested_plural}#index")
62
- get "/#{plural}/:id/#{nested_plural}/new", options.merge(to: "#{nested_plural}#new")
63
- post "/#{plural}/:id/#{nested_plural}", options.merge(to: "#{nested_plural}#create")
64
- get "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#show")
65
- get "/#{plural}/:id/#{nested_plural}/:nested_id/edit", options.merge(to: "#{nested_plural}#edit")
66
- put "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#update")
67
- patch "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#update")
68
- delete "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#destroy")
69
- end
61
+ include RubyRoutes::Router::HttpHelpers
70
62
 
71
- # Handle concerns if block is given
72
- if block_given?
73
- # Push a scope for nested resources
74
- @scope_stack.push({ path: "/#{plural}/:id" })
75
- # Execute the block in the context of this router instance
76
- instance_eval(&block)
77
- @scope_stack.pop
78
- end
63
+ # Initialize the router.
64
+ #
65
+ # @param definition_block [Proc] The block to define routes.
66
+ def initialize(&definition_block)
67
+ @route_set = RouteSet.new
68
+ @route_utils = RubyRoutes::Utility::RouteUtility.new(@route_set)
69
+ @scope_stack = []
70
+ @concerns = {}
71
+ instance_eval(&definition_block) if definition_block
79
72
  end
80
73
 
81
- def resource(name, options = {})
82
- singular = name.to_s.singularize
83
-
84
- get "/#{singular}", options.merge(to: "#{singular}#show")
85
- get "/#{singular}/new", options.merge(to: "#{singular}#new")
86
- post "/#{singular}", options.merge(to: "#{singular}#create")
87
- get "/#{singular}/edit", options.merge(to: "#{singular}#edit")
88
- put "/#{singular}", options.merge(to: "#{singular}#update")
89
- patch "/#{singular}", options.merge(to: "#{singular}#update")
90
- delete "/#{singular}", options.merge(to: "#{singular}#destroy")
74
+ # Build a finalized router.
75
+ #
76
+ # @param definition_block [Proc] The block to define routes.
77
+ # @return [Router] The finalized router.
78
+ def self.build(&definition_block)
79
+ new(&definition_block).finalize!
91
80
  end
92
81
 
93
- # Namespace support
94
- def namespace(name, options = {}, &block)
95
- @scope_stack.push({ path: "/#{name}", module: name })
96
-
97
- if block_given?
98
- instance_eval(&block)
99
- end
82
+ # Finalize router for DSL immutability.
83
+ #
84
+ # @return [Router] self.
85
+ def finalize!
86
+ return self if @frozen
100
87
 
101
- @scope_stack.pop
88
+ @frozen = true
89
+ @scope_stack.freeze
90
+ @concerns.freeze
91
+ self
102
92
  end
103
93
 
104
- # Scope support
105
- def scope(options = {}, &block)
106
- @scope_stack.push(options)
107
-
108
- if block_given?
109
- instance_eval(&block)
110
- end
111
-
112
- @scope_stack.pop
94
+ # Check if the router is frozen.
95
+ #
96
+ # @return [Boolean] `true` if the router is frozen, `false` otherwise.
97
+ def frozen?
98
+ !!@frozen
113
99
  end
114
100
 
115
- # Root route
101
+ # Define a root route.
102
+ #
103
+ # @param options [Hash] The options for the root route.
104
+ # @return [Router] self.
116
105
  def root(options = {})
117
- add_route("/", options.merge(via: :get))
118
- end
119
-
120
- # Concerns (reusable route groups)
121
- def concerns(*names, &block)
122
- names.each do |name|
123
- concern = @concerns[name]
124
- raise "Concern '#{name}' not found" unless concern
125
-
126
- instance_eval(&concern)
127
- end
128
-
129
- if block_given?
130
- instance_eval(&block)
106
+ add_route('/', build_route_options(options, :get))
107
+ self
108
+ end
109
+
110
+ # ---- RESTful Resources -------------------------------------------------
111
+
112
+ # Define RESTful resources.
113
+ #
114
+ # @param resource_name [Symbol, String] The resource name.
115
+ # @param options [Hash] The options for the resource.
116
+ # @param nested_block [Proc] The block for nested routes.
117
+ # @return [Router] self.
118
+ def resources(resource_name, options = {}, &nested_block)
119
+ define_resource_routes(resource_name, options, &nested_block)
120
+ end
121
+
122
+ # Define a singular resource.
123
+ #
124
+ # @param resource_name [Symbol, String] The resource name.
125
+ # @param options [Hash] The options for the resource.
126
+ # @return [Router] self.
127
+ def resource(resource_name, options = {})
128
+ singular = RubyRoutes::Utility::InflectorUtility.singularize(resource_name.to_s)
129
+ controller = options[:controller] || singular
130
+ define_singular_routes(singular, controller, options)
131
+ end
132
+
133
+ # ---- Scoping & Namespaces ----------------------------------------------
134
+
135
+ # Define a namespace.
136
+ #
137
+ # @param namespace_name [Symbol, String] The namespace name.
138
+ # @param options [Hash] The options for the namespace.
139
+ # @param block [Proc] The block for nested routes.
140
+ # @return [Router] self.
141
+ def namespace(namespace_name, options = {}, &block)
142
+ push_scope({ path: "/#{namespace_name}", module: namespace_name }.merge(options)) do
143
+ instance_eval(&block) if block
131
144
  end
132
145
  end
133
146
 
134
- def concern(name, &block)
135
- @concerns[name] = block
136
- end
137
-
138
- # Route constraints
139
- def constraints(constraints = {}, &block)
140
- @scope_stack.push({ constraints: constraints })
141
-
142
- if block_given?
143
- instance_eval(&block)
147
+ # Define a scope.
148
+ #
149
+ # @param options_or_path [Hash, String] The options or path for the scope.
150
+ # @param block [Proc] The block for nested routes.
151
+ # @return [Router] self.
152
+ def scope(options_or_path = {}, &block)
153
+ scope_entry = options_or_path.is_a?(String) ? { path: options_or_path } : options_or_path
154
+ push_scope(scope_entry) { instance_eval(&block) if block }
155
+ end
156
+
157
+ # Define constraints.
158
+ #
159
+ # @param constraints_hash [Hash] The constraints for the scope.
160
+ # @param block [Proc] The block for nested routes.
161
+ # @return [Router] self.
162
+ def constraints(constraints_hash = {}, &block)
163
+ push_scope(constraints: constraints_hash) { instance_eval(&block) if block }
164
+ end
165
+
166
+ # Define defaults.
167
+ #
168
+ # @param defaults_hash [Hash] The default values for the scope.
169
+ # @param block [Proc] The block for nested routes.
170
+ # @return [Router] self.
171
+ def defaults(defaults_hash = {}, &block)
172
+ push_scope(defaults: defaults_hash) { instance_eval(&block) if block }
173
+ end
174
+
175
+ # ---- Concerns ----------------------------------------------------------
176
+
177
+ # Define a concern.
178
+ #
179
+ # @param concern_name [Symbol] The concern name.
180
+ # @param block [Proc] The block defining the concern.
181
+ # @return [void]
182
+ def concern(concern_name, &block)
183
+ ensure_unfrozen!
184
+ @concerns[concern_name] = block
185
+ end
186
+
187
+ # Use concerns.
188
+ #
189
+ # @param concern_names [Array<Symbol>] The names of the concerns to use.
190
+ # @param block [Proc] The block for additional routes.
191
+ # @return [void]
192
+ def concerns(*concern_names, &block)
193
+ concern_names.each do |name|
194
+ concern_block = @concerns[name]
195
+ raise "Concern '#{name}' not found" unless concern_block
196
+
197
+ instance_eval(&concern_block)
144
198
  end
145
-
146
- @scope_stack.pop
199
+ instance_eval(&block) if block
147
200
  end
148
201
 
149
- # Defaults
150
- def defaults(defaults = {}, &block)
151
- @scope_stack.push({ defaults: defaults })
152
-
153
- if block_given?
154
- instance_eval(&block)
155
- end
156
-
157
- @scope_stack.pop
158
- end
202
+ # ---- Mounting ----------------------------------------------------------
159
203
 
160
- # Mount other applications
204
+ # Mount an app.
205
+ #
206
+ # @param app [Object] The app to mount.
207
+ # @param at [String, nil] The path to mount the app at.
208
+ # @return [void]
161
209
  def mount(app, at: nil)
162
- path = at || "/#{app}"
163
- add_route("#{path}/*path", to: app, via: :all)
164
- end
165
-
166
- private
167
-
168
- def add_route(path, options = {})
169
- # Apply current scope
170
- scoped_options = apply_scope(path, options)
171
-
172
- # Create and add the route
173
- route = Route.new(scoped_options[:path], scoped_options)
174
- @route_set.add_route(route)
175
- route
176
- end
177
-
178
- def apply_scope(path, options)
179
- scoped_options = options.dup
180
- scoped_path = path
181
-
182
- @scope_stack.reverse_each do |scope|
183
- if scope[:path]
184
- scoped_path = "#{scope[:path]}#{scoped_path}"
185
- end
186
-
187
- if scope[:module] && scoped_options[:to]
188
- controller = scoped_options[:to].to_s.split('#').first
189
- scoped_options[:to] = "#{scope[:module]}/#{controller}##{scoped_options[:to].to_s.split('#').last}"
190
- end
191
-
192
- if scope[:constraints]
193
- scoped_options[:constraints] = (scoped_options[:constraints] || {}).merge(scope[:constraints])
194
- end
195
-
196
- if scope[:defaults]
197
- scoped_options[:defaults] = (scoped_options[:defaults] || {}).merge(scope[:defaults])
198
- end
199
- end
200
-
201
- scoped_options[:path] = scoped_path
202
- scoped_options
210
+ ensure_unfrozen!
211
+ mount_path = at || "/#{app}"
212
+ defaults = { _mounted_app: app }
213
+ add_route(
214
+ "#{mount_path}/*path",
215
+ controller: 'mounted',
216
+ action: :call,
217
+ via: VERBS_ALL,
218
+ defaults: defaults
219
+ )
203
220
  end
204
221
  end
205
222
  end
@@ -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'
@@ -5,16 +7,34 @@ require_relative 'segments/wildcard_segment'
5
7
  require_relative 'constant'
6
8
 
7
9
  module RubyRoutes
10
+ # Segment
11
+ #
12
+ # Factory wrapper that selects the correct concrete Segment subclass
13
+ # (Static / Dynamic / Wildcard) based on the first character of a raw
14
+ # path token:
15
+ # - ":" → DynamicSegment (named param, e.g. :id)
16
+ # - "*" → WildcardSegment (greedy splat, e.g. *path)
17
+ # - otherwise → StaticSegment (literal text)
18
+ #
19
+ # It delegates byte‑based dispatch to RubyRoutes::Constant::SEGMENTS
20
+ # for O(1) lookup without multiple string comparisons.
21
+ #
22
+ # @api internal
8
23
  class Segment
24
+ # Build an appropriate segment instance for the provided token.
25
+ #
26
+ # @param text [String, Symbol, #to_s] raw segment token
27
+ # @return [RubyRoutes::Segments::BaseSegment]
28
+ #
29
+ # @example
30
+ # Segment.for(":id") # => DynamicSegment
31
+ # Segment.for("*files") # => WildcardSegment
32
+ # Segment.for("users") # => StaticSegment
9
33
  def self.for(text)
10
- t = text.to_s
11
- key = t.empty? ? :default : t.getbyte(0)
12
- segment = RubyRoutes::Constant::SEGMENTS[key] || RubyRoutes::Constant::SEGMENTS[:default]
13
- segment.new(t)
14
- end
15
-
16
- def wildcard?
17
- false
34
+ segment_text = text.to_s
35
+ segment_key = segment_text.empty? ? :default : segment_text.getbyte(0)
36
+ segment_class = RubyRoutes::Constant::SEGMENTS[segment_key] || RubyRoutes::Constant::SEGMENTS[:default]
37
+ segment_class.new(segment_text)
18
38
  end
19
39
  end
20
40
  end
@@ -1,19 +1,55 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RubyRoutes
2
4
  module Segments
5
+ # BaseSegment
6
+ #
7
+ # Abstract superclass for all parsed route path segments.
8
+ # Concrete subclasses implement:
9
+ # - StaticSegment: literal path component
10
+ # - DynamicSegment: parameter capture (e.g. :id)
11
+ # - WildcardSegment: greedy capture (e.g. *path)
12
+ #
13
+ # Responsibilities shared by subclasses:
14
+ # - Supplying a child node in the radix tree (ensure_child)
15
+ # - Participating in traversal / matching (match)
16
+ #
17
+ # Subclasses must override #ensure_child and #match.
18
+ #
19
+ # @api internal
3
20
  class BaseSegment
4
- def initialize(text = nil)
5
- @text = text.to_s if text
21
+ # @param raw_segment_text [String, Symbol, nil]
22
+ def initialize(raw_segment_text = nil)
23
+ @raw_text = raw_segment_text.to_s if raw_segment_text
6
24
  end
7
25
 
26
+ # Indicates whether this segment is a wildcard (greedy) segment.
27
+ #
28
+ # @return [Boolean]
8
29
  def wildcard?
9
30
  false
10
31
  end
11
32
 
12
- def ensure_child(current)
33
+ # Ensure the proper child node exists beneath +parent_node+ for this segment
34
+ # and return it.
35
+ #
36
+ # @param parent_node [Object] radix tree node (implementation-specific)
37
+ # @return [Object] the (possibly newly-created) child node
38
+ # @raise [NotImplementedError] when not overridden
39
+ def ensure_child(parent_node)
13
40
  raise NotImplementedError, "#{self.class}#ensure_child must be implemented"
14
41
  end
15
42
 
16
- def match(_node, _text, _idx, _segments, _params)
43
+ # Attempt to match this segment during traversal.
44
+ #
45
+ # @param current_node [Object] the current radix node
46
+ # @param incoming_segment_text [String] the path component being matched
47
+ # @param _segment_index [Integer] index of the component in the path (unused here)
48
+ # @param _all_segments [Array<String>] full list of segments (unused here)
49
+ # @param _captured_params [Hash, nil] params hash to populate (unused here)
50
+ # @return [Array<(Object, Boolean)>] [next_node, stop_traversal]
51
+ # @raise [NotImplementedError] when not overridden
52
+ def match(current_node, incoming_segment_text, _segment_index, _all_segments, _captured_params)
17
53
  raise NotImplementedError, "#{self.class}#match must be implemented"
18
54
  end
19
55
  end
@@ -1,22 +1,58 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RubyRoutes
2
4
  module Segments
5
+ # DynamicSegment
6
+ #
7
+ # Represents a single dynamic (named) path component in a route
8
+ # definition (e.g. ":id" in "/users/:id").
9
+ #
10
+ # Responsibilities:
11
+ # - Ensures a dynamic_child node exists in the radix tree for traversal.
12
+ # - On match, captures the actual segment text into the params hash
13
+ # using the parameter name (without the leading colon).
14
+ #
15
+ # Matching Behavior:
16
+ # - Succeeds if the current radix node has a dynamic_child.
17
+ # - Does NOT stop traversal (returns false as second tuple value).
18
+ #
19
+ # Returned tuple from #match:
20
+ # [next_node, stop_traversal_flag]
21
+ #
22
+ # @api internal
3
23
  class DynamicSegment < BaseSegment
4
- def initialize(text)
5
- @name = text[1..-1]
24
+ # @param raw_segment_text [String] raw token (e.g. ":id")
25
+ def initialize(raw_segment_text)
26
+ super(raw_segment_text)
27
+ @param_name = raw_segment_text[1..]
6
28
  end
7
29
 
8
- def ensure_child(current)
9
- current.dynamic_child ||= Node.new
10
- current = current.dynamic_child
11
- current.param_name = @name
12
- current
30
+ # Ensure a dynamic child node under +parent_node+ and assign the
31
+ # parameter name to that node for later extraction.
32
+ #
33
+ # @param parent_node [Object] radix tree node
34
+ # @return [Object] the dynamic child node
35
+ def ensure_child(parent_node)
36
+ parent_node.dynamic_child ||= Node.new
37
+ dynamic_child_node = parent_node.dynamic_child
38
+ dynamic_child_node.param_name = @param_name
39
+ dynamic_child_node
13
40
  end
14
41
 
15
- def match(node, text, _idx, _segments, params)
16
- return [nil, false] unless node.dynamic_child
17
- nxt = node.dynamic_child
18
- params[nxt.param_name.to_s] = text if params
19
- [nxt, false]
42
+ # Attempt to match this segment during traversal.
43
+ #
44
+ # @param current_node [Object] current radix node
45
+ # @param incoming_segment_text [String] actual path segment from request
46
+ # @param _segment_index [Integer] (unused)
47
+ # @param _all_segments [Array<String>] (unused)
48
+ # @param captured_params [Hash] params hash to populate
49
+ # @return [Array<(Object, Boolean)>] [next_node, stop_traversal=false]
50
+ def match(current_node, incoming_segment_text, _segment_index, _all_segments, captured_params)
51
+ return [nil, false] unless current_node.dynamic_child
52
+
53
+ dynamic_child_node = current_node.dynamic_child
54
+ captured_params[dynamic_child_node.param_name.to_s] = incoming_segment_text if captured_params
55
+ [dynamic_child_node, false]
20
56
  end
21
57
  end
22
58
  end
@@ -1,17 +1,53 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RubyRoutes
2
4
  module Segments
5
+ # StaticSegment
6
+ #
7
+ # Represents a literal (non-parameter) segment in a route path.
8
+ # Example: "users" in "/users/:id".
9
+ #
10
+ # Responsibilities:
11
+ # - Ensures a static child node exists under the current radix node
12
+ # keyed by the literal segment text.
13
+ # - During traversal (#match), returns the child node for the incoming
14
+ # path component if it exists.
15
+ #
16
+ # Matching Behavior:
17
+ # - Succeeds only when the exact literal exists as a child.
18
+ # - Never captures parameters (no changes to params hash).
19
+ # - Never stops traversal early (second tuple element = false).
20
+ #
21
+ # Returned tuple from #match:
22
+ # [next_node, stop_traversal_flag]
23
+ #
24
+ # @api internal
3
25
  class StaticSegment < BaseSegment
4
- def initialize(text)
5
- @text = text
26
+ # @param raw_segment_text [String] literal segment token
27
+ def initialize(raw_segment_text)
28
+ super(raw_segment_text)
29
+ @literal_text = raw_segment_text
6
30
  end
7
31
 
8
- def ensure_child(current)
9
- current.static_children[@text] ||= Node.new
10
- current.static_children[@text]
32
+ # Ensure a static child node for this literal under +parent_node+.
33
+ #
34
+ # @param parent_node [Object] radix tree node
35
+ # @return [Object] the static child node
36
+ def ensure_child(parent_node)
37
+ parent_node.static_children[@literal_text] ||= Node.new
38
+ parent_node.static_children[@literal_text]
11
39
  end
12
40
 
13
- def match(node, text, _idx, _segments, _params)
14
- [node.static_children[text], false]
41
+ # Attempt to match this literal segment.
42
+ #
43
+ # @param current_node [Object] current radix node
44
+ # @param incoming_segment_text [String] segment from request path
45
+ # @param _segment_index [Integer] (unused)
46
+ # @param _all_segments [Array<String>] (unused)
47
+ # @param _extracted_params [Hash] (unused, no params captured)
48
+ # @return [Array<(Object, Boolean)>] [next_node_or_nil, stop_traversal=false]
49
+ def match(current_node, incoming_segment_text, _segment_index, _all_segments, _extracted_params)
50
+ [current_node.static_children[incoming_segment_text], false]
15
51
  end
16
52
  end
17
53
  end