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,210 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utility/inflector_utility'
1
4
  require_relative 'utility/route_utility'
5
+ require_relative 'router/http_helpers'
2
6
 
3
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
4
56
  class Router
57
+ VERBS_ALL = RubyRoutes::Constant::VERBS_ALL
58
+
5
59
  attr_reader :route_set
6
60
 
7
- def initialize(&block)
8
- @route_set = RouteSet.new
61
+ include RubyRoutes::Router::HttpHelpers
62
+
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
9
68
  @route_utils = RubyRoutes::Utility::RouteUtility.new(@route_set)
10
69
  @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))
70
+ @concerns = {}
71
+ instance_eval(&definition_block) if definition_block
22
72
  end
23
73
 
24
- def put(path, options = {})
25
- add_route(path, options.merge(via: :put))
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!
26
80
  end
27
81
 
28
- def patch(path, options = {})
29
- add_route(path, options.merge(via: :patch))
30
- end
82
+ # Finalize router for DSL immutability.
83
+ #
84
+ # @return [Router] self.
85
+ def finalize!
86
+ return self if @frozen
31
87
 
32
- def delete(path, options = {})
33
- add_route(path, options.merge(via: :delete))
88
+ @frozen = true
89
+ @scope_stack.freeze
90
+ @concerns.freeze
91
+ self
34
92
  end
35
93
 
36
- def match(path, options = {})
37
- add_route(path, options)
94
+ # Check if the router is frozen.
95
+ #
96
+ # @return [Boolean] `true` if the router is frozen, `false` otherwise.
97
+ def frozen?
98
+ !!@frozen
38
99
  end
39
100
 
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
123
- end
124
-
125
- # Root route
101
+ # Define a root route.
102
+ #
103
+ # @param options [Hash] The options for the root route.
104
+ # @return [Router] self.
126
105
  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)
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
141
144
  end
142
145
  end
143
146
 
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)
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)
154
198
  end
155
-
156
- @scope_stack.pop
199
+ instance_eval(&block) if block
157
200
  end
158
201
 
159
- # Defaults
160
- def defaults(defaults = {}, &block)
161
- @scope_stack.push({ defaults: defaults })
162
-
163
- if block_given?
164
- instance_eval(&block)
165
- end
166
-
167
- @scope_stack.pop
168
- end
202
+ # ---- Mounting ----------------------------------------------------------
169
203
 
170
- # 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]
171
209
  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
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
+ )
208
220
  end
209
221
  end
210
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