ruby_routes 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +240 -163
  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 +86 -36
  7. data/lib/ruby_routes/radix_tree/finder.rb +213 -0
  8. data/lib/ruby_routes/radix_tree/inserter.rb +96 -0
  9. data/lib/ruby_routes/radix_tree.rb +65 -230
  10. data/lib/ruby_routes/route/check_helpers.rb +115 -0
  11. data/lib/ruby_routes/route/constraint_validator.rb +173 -0
  12. data/lib/ruby_routes/route/param_support.rb +200 -0
  13. data/lib/ruby_routes/route/path_builder.rb +84 -0
  14. data/lib/ruby_routes/route/path_generation.rb +87 -0
  15. data/lib/ruby_routes/route/query_helpers.rb +56 -0
  16. data/lib/ruby_routes/route/segment_compiler.rb +166 -0
  17. data/lib/ruby_routes/route/small_lru.rb +93 -18
  18. data/lib/ruby_routes/route/validation_helpers.rb +174 -0
  19. data/lib/ruby_routes/route/warning_helpers.rb +57 -0
  20. data/lib/ruby_routes/route.rb +127 -501
  21. data/lib/ruby_routes/route_set/cache_helpers.rb +76 -0
  22. data/lib/ruby_routes/route_set/collection_helpers.rb +125 -0
  23. data/lib/ruby_routes/route_set.rb +140 -132
  24. data/lib/ruby_routes/router/build_helpers.rb +99 -0
  25. data/lib/ruby_routes/router/builder.rb +97 -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 +127 -0
  29. data/lib/ruby_routes/router.rb +196 -182
  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 +58 -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 +171 -77
  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,210 +1,224 @@
1
- require_relative 'utility/route_utility'
1
+ # frozen_string_literal: true
2
2
 
3
+ require_relative 'utility/inflector_utility'
4
+ require_relative 'utility/route_utility'
5
+ require_relative 'router/http_helpers'
6
+ require_relative 'constant'
7
+ require_relative 'route_set'
3
8
  module RubyRoutes
9
+ # RubyRoutes::Router
10
+ #
11
+ # Public DSL entrypoint for defining application routes.
12
+ #
13
+ # Usage:
14
+ # router = RubyRoutes::Router.new do
15
+ # get '/health', to: 'system#health'
16
+ # resources :users
17
+ # namespace :admin do
18
+ # resources :posts
19
+ # end
20
+ # end
21
+ #
22
+ # Thread Safety:
23
+ # Build routes at boot. Mutating after multiple threads start serving
24
+ # requests is not supported.
25
+ #
26
+ # Responsibilities:
27
+ # - Provide Rails‑inspired DSL (get/post/put/patch/delete/match/root).
28
+ # - Define RESTful collections via `#resources` and singular via `#resource`.
29
+ # - Support scoping (namespace / scope / constraints / defaults).
30
+ # - Allow reusable blocks via concerns (`#concern` / `#concerns`).
31
+ # - Mount external Rack apps (`#mount`).
32
+ # - Delegate route object creation & storage to `RouteSet` / `RouteUtility`.
33
+ #
34
+ # Design Notes:
35
+ # - Scope stack is an array of shallow hashes (path/module/constraints/defaults).
36
+ # - Scopes are applied inner-first (reverse_each). For options (constraints/defaults),
37
+ # inner values should override outer ones.
38
+ # - Options hashes passed by users are duplicated only when necessary
39
+ # (see `build_route_options`) to reduce allocation churn.
40
+ #
41
+ # Public API Surface (Stable):
42
+ # - `#initialize` (block form)
43
+ # - HTTP verb helpers (`get/post/put/patch/delete/match`)
44
+ # - `#root`
45
+ # - `#resources` / `#resource`
46
+ # - `#namespace` / `#scope` / `#constraints` / `#defaults`
47
+ # - `#concern` / `#concerns`
48
+ # - `#mount`
49
+ #
50
+ # Internal / Subject to Change:
51
+ # - `#add_route`
52
+ # - `#apply_scope`
53
+ # - `#build_route_options`
54
+ # - `#push_scope`
55
+ #
56
+ # @api public
4
57
  class Router
58
+ VERBS_ALL = RubyRoutes::Constant::VERBS_ALL
59
+
5
60
  attr_reader :route_set
6
61
 
7
- def initialize(&block)
8
- @route_set = RouteSet.new
62
+ include RubyRoutes::Router::HttpHelpers
63
+
64
+ # Initialize the router.
65
+ #
66
+ # @param definition_block [Proc] The block to define routes.
67
+ def initialize(&definition_block)
68
+ @route_set = RouteSet.new
9
69
  @route_utils = RubyRoutes::Utility::RouteUtility.new(@route_set)
10
70
  @scope_stack = []
11
- @concerns = {}
12
- instance_eval(&block) if block_given?
13
- end
14
-
15
- # Basic route definition
16
- def get(path, options = {})
17
- add_route(path, options.merge(via: :get))
18
- end
19
-
20
- def post(path, options = {})
21
- add_route(path, options.merge(via: :post))
22
- end
23
-
24
- def put(path, options = {})
25
- add_route(path, options.merge(via: :put))
71
+ @concerns = {}
72
+ instance_eval(&definition_block) if definition_block
26
73
  end
27
74
 
28
- def patch(path, options = {})
29
- add_route(path, options.merge(via: :patch))
75
+ # Build a finalized router.
76
+ #
77
+ # @param definition_block [Proc] The block to define routes.
78
+ # @return [Router] The finalized router.
79
+ def self.build(&definition_block)
80
+ new(&definition_block).finalize!
30
81
  end
31
82
 
32
- def delete(path, options = {})
33
- add_route(path, options.merge(via: :delete))
34
- end
83
+ # Finalize router for DSL immutability.
84
+ #
85
+ # @return [Router] self.
86
+ def finalize!
87
+ return self if @frozen
35
88
 
36
- def match(path, options = {})
37
- add_route(path, options)
89
+ @frozen = true
90
+ @scope_stack.freeze
91
+ @concerns.freeze
92
+ self
38
93
  end
39
94
 
40
- # Resources routing (Rails-like)
41
- def resources(name, options = {}, &block)
42
- singular = name.to_s.singularize
43
- plural = (options[:path] || name.to_s.pluralize)
44
- controller = options[:controller] || plural
45
-
46
- # Collection routes
47
- get "/#{plural}", options.merge(to: "#{controller}#index")
48
- get "/#{plural}/new", options.merge(to: "#{controller}#new")
49
- post "/#{plural}", options.merge(to: "#{controller}#create")
50
-
51
- # Member routes
52
- get "/#{plural}/:id", options.merge(to: "#{controller}#show")
53
- get "/#{plural}/:id/edit", options.merge(to: "#{controller}#edit")
54
- put "/#{plural}/:id", options.merge(to: "#{controller}#update")
55
- patch "/#{plural}/:id", options.merge(to: "#{controller}#update")
56
- delete "/#{plural}/:id", options.merge(to: "#{controller}#destroy")
57
-
58
- # Nested resources if specified
59
- if options[:nested]
60
- nested_name = options[:nested]
61
- nested_singular = nested_name.to_s.singularize
62
- nested_plural = nested_name.to_s.pluralize
63
-
64
- get "/#{plural}/:id/#{nested_plural}", options.merge(to: "#{nested_plural}#index")
65
- get "/#{plural}/:id/#{nested_plural}/new", options.merge(to: "#{nested_plural}#new")
66
- post "/#{plural}/:id/#{nested_plural}", options.merge(to: "#{nested_plural}#create")
67
- get "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#show")
68
- get "/#{plural}/:id/#{nested_plural}/:nested_id/edit", options.merge(to: "#{nested_plural}#edit")
69
- put "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#update")
70
- patch "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#update")
71
- delete "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#destroy")
72
- end
73
-
74
- # Handle concerns if block is given
75
- if block_given?
76
- # Push a scope for nested resources
77
- @scope_stack.push({ path: "/#{plural}/:id" })
78
- # Execute the block in the context of this router instance
79
- instance_eval(&block)
80
- @scope_stack.pop
81
- end
82
- end
83
-
84
- def resource(name, options = {})
85
- singular = name.to_s.singularize
86
-
87
- get "/#{singular}", options.merge(to: "#{singular}#show")
88
- get "/#{singular}/new", options.merge(to: "#{singular}#new")
89
- post "/#{singular}", options.merge(to: "#{singular}#create")
90
- get "/#{singular}/edit", options.merge(to: "#{singular}#edit")
91
- put "/#{singular}", options.merge(to: "#{singular}#update")
92
- patch "/#{singular}", options.merge(to: "#{singular}#update")
93
- delete "/#{singular}", options.merge(to: "#{singular}#destroy")
94
- end
95
-
96
- # Namespace support
97
- def namespace(name, options = {}, &block)
98
- @scope_stack.push({ path: "/#{name}", module: name })
99
-
100
- if block_given?
101
- instance_eval(&block)
102
- end
103
-
104
- @scope_stack.pop
105
- end
106
-
107
- # Scope support
108
- def scope(options_or_path = {}, &block)
109
- # Handle the case where the first argument is a string (path)
110
- options = if options_or_path.is_a?(String)
111
- { path: options_or_path }
112
- else
113
- options_or_path
114
- end
115
-
116
- @scope_stack.push(options)
117
-
118
- if block_given?
119
- instance_eval(&block)
120
- end
121
-
122
- @scope_stack.pop
95
+ # Check if the router is frozen.
96
+ #
97
+ # @return [Boolean] `true` if the router is frozen, `false` otherwise.
98
+ def frozen?
99
+ !!@frozen
123
100
  end
124
101
 
125
- # Root route
102
+ # Define a root route.
103
+ #
104
+ # @param options [Hash] The options for the root route.
105
+ # @return [Router] self.
126
106
  def root(options = {})
127
- add_route("/", options.merge(via: :get))
128
- end
129
-
130
- # Concerns (reusable route groups)
131
- def concerns(*names, &block)
132
- names.each do |name|
133
- concern = @concerns[name]
134
- raise "Concern '#{name}' not found" unless concern
135
-
136
- instance_eval(&concern)
137
- end
138
-
139
- if block_given?
140
- instance_eval(&block)
107
+ add_route('/', build_route_options(options, :get))
108
+ self
109
+ end
110
+
111
+ # ---- RESTful Resources -------------------------------------------------
112
+
113
+ # Define RESTful resources.
114
+ #
115
+ # @param resource_name [Symbol, String] The resource name.
116
+ # @param options [Hash] The options for the resource.
117
+ # @param nested_block [Proc] The block for nested routes.
118
+ # @return [Router] self.
119
+ def resources(resource_name, options = {}, &nested_block)
120
+ define_resource_routes(resource_name, options, &nested_block)
121
+ self
122
+ end
123
+
124
+ # Define a singular resource.
125
+ #
126
+ # @param resource_name [Symbol, String] The resource name.
127
+ # @param options [Hash] The options for the resource.
128
+ # @return [Router] self.
129
+ def resource(resource_name, options = {})
130
+ singular = RubyRoutes::Utility::InflectorUtility.singularize(resource_name.to_s)
131
+ controller = options[:controller] || singular
132
+ define_singular_routes(singular, controller, options)
133
+ end
134
+
135
+ # ---- Scoping & Namespaces ----------------------------------------------
136
+
137
+ # Define a namespace.
138
+ #
139
+ # @param namespace_name [Symbol, String] The namespace name.
140
+ # @param options [Hash] The options for the namespace.
141
+ # @param block [Proc] The block for nested routes.
142
+ # @return [Router] self.
143
+ def namespace(namespace_name, options = {}, &block)
144
+ push_scope({ path: "/#{namespace_name}", module: namespace_name }.merge(options)) do
145
+ instance_eval(&block) if block
141
146
  end
142
147
  end
143
148
 
144
- def concern(name, &block)
145
- @concerns[name] = block
146
- end
147
-
148
- # Route constraints
149
- def constraints(constraints = {}, &block)
150
- @scope_stack.push({ constraints: constraints })
151
-
152
- if block_given?
153
- instance_eval(&block)
149
+ # Define a scope.
150
+ #
151
+ # @param options_or_path [Hash, String] The options or path for the scope.
152
+ # @param block [Proc] The block for nested routes.
153
+ # @return [Router] self.
154
+ def scope(options_or_path = {}, &block)
155
+ scope_entry = options_or_path.is_a?(String) ? { path: options_or_path } : options_or_path
156
+ push_scope(scope_entry) { instance_eval(&block) if block }
157
+ end
158
+
159
+ # Define constraints.
160
+ #
161
+ # @param constraints_hash [Hash] The constraints for the scope.
162
+ # @param block [Proc] The block for nested routes.
163
+ # @return [Router] self.
164
+ def constraints(constraints_hash = {}, &block)
165
+ push_scope(constraints: constraints_hash) { instance_eval(&block) if block }
166
+ end
167
+
168
+ # Define defaults.
169
+ #
170
+ # @param defaults_hash [Hash] The default values for the scope.
171
+ # @param block [Proc] The block for nested routes.
172
+ # @return [Router] self.
173
+ def defaults(defaults_hash = {}, &block)
174
+ push_scope(defaults: defaults_hash) { instance_eval(&block) if block }
175
+ end
176
+
177
+ # ---- Concerns ----------------------------------------------------------
178
+
179
+ # Define a concern.
180
+ #
181
+ # @param concern_name [Symbol] The concern name.
182
+ # @param block [Proc] The block defining the concern.
183
+ # @return [void]
184
+ def concern(concern_name, &block)
185
+ ensure_unfrozen!
186
+ @concerns[concern_name] = block
187
+ end
188
+
189
+ # Use concerns.
190
+ #
191
+ # @param concern_names [Array<Symbol>] The names of the concerns to use.
192
+ # @param block [Proc] The block for additional routes.
193
+ # @return [void]
194
+ def concerns(*concern_names, &block)
195
+ concern_names.each do |name|
196
+ concern_block = @concerns[name]
197
+ raise "Concern '#{name}' not found" unless concern_block
198
+
199
+ instance_eval(&concern_block)
154
200
  end
155
-
156
- @scope_stack.pop
201
+ instance_eval(&block) if block
157
202
  end
158
203
 
159
- # Defaults
160
- def defaults(defaults = {}, &block)
161
- @scope_stack.push({ defaults: defaults })
162
-
163
- if block_given?
164
- instance_eval(&block)
165
- end
204
+ # ---- Mounting ----------------------------------------------------------
166
205
 
167
- @scope_stack.pop
168
- end
169
-
170
- # Mount other applications
206
+ # Mount an app.
207
+ #
208
+ # @param app [Object] The app to mount.
209
+ # @param at [String, nil] The path to mount the app at.
210
+ # @return [void]
171
211
  def mount(app, at: nil)
172
- path = at || "/#{app}"
173
- add_route("#{path}/*path", to: app, via: :all)
174
- end
175
-
176
- private
177
-
178
- def add_route(path, options={})
179
- scoped = apply_scope(path, options)
180
- @route_utils.define(scoped[:path], scoped)
181
- end
182
-
183
- def apply_scope(path, options)
184
- scoped_options = options.dup
185
- scoped_path = path
186
-
187
- @scope_stack.reverse_each do |scope|
188
- if scope[:path]
189
- scoped_path = "#{scope[:path]}#{scoped_path}"
190
- end
191
-
192
- if scope[:module] && scoped_options[:to]
193
- controller = scoped_options[:to].to_s.split('#').first
194
- scoped_options[:to] = "#{scope[:module]}/#{controller}##{scoped_options[:to].to_s.split('#').last}"
195
- end
196
-
197
- if scope[:constraints]
198
- scoped_options[:constraints] = (scoped_options[:constraints] || {}).merge(scope[:constraints])
199
- end
200
-
201
- if scope[:defaults]
202
- scoped_options[:defaults] = (scoped_options[:defaults] || {}).merge(scope[:defaults])
203
- end
204
- end
205
-
206
- scoped_options[:path] = scoped_path
207
- scoped_options
212
+ ensure_unfrozen!
213
+ mount_path = at || "/#{app}"
214
+ defaults = { _mounted_app: app }
215
+ add_route(
216
+ "#{mount_path}/*path",
217
+ controller: 'mounted',
218
+ action: :call,
219
+ via: VERBS_ALL,
220
+ defaults: defaults
221
+ )
208
222
  end
209
223
  end
210
224
  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