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.
- checksums.yaml +4 -4
- data/README.md +240 -163
- data/lib/ruby_routes/constant.rb +137 -18
- data/lib/ruby_routes/lru_strategies/hit_strategy.rb +31 -4
- data/lib/ruby_routes/lru_strategies/miss_strategy.rb +21 -0
- data/lib/ruby_routes/node.rb +86 -36
- data/lib/ruby_routes/radix_tree/finder.rb +213 -0
- data/lib/ruby_routes/radix_tree/inserter.rb +96 -0
- data/lib/ruby_routes/radix_tree.rb +65 -230
- data/lib/ruby_routes/route/check_helpers.rb +115 -0
- data/lib/ruby_routes/route/constraint_validator.rb +173 -0
- data/lib/ruby_routes/route/param_support.rb +200 -0
- data/lib/ruby_routes/route/path_builder.rb +84 -0
- data/lib/ruby_routes/route/path_generation.rb +87 -0
- data/lib/ruby_routes/route/query_helpers.rb +56 -0
- data/lib/ruby_routes/route/segment_compiler.rb +166 -0
- data/lib/ruby_routes/route/small_lru.rb +93 -18
- data/lib/ruby_routes/route/validation_helpers.rb +174 -0
- data/lib/ruby_routes/route/warning_helpers.rb +57 -0
- data/lib/ruby_routes/route.rb +127 -501
- data/lib/ruby_routes/route_set/cache_helpers.rb +76 -0
- data/lib/ruby_routes/route_set/collection_helpers.rb +125 -0
- data/lib/ruby_routes/route_set.rb +140 -132
- data/lib/ruby_routes/router/build_helpers.rb +99 -0
- data/lib/ruby_routes/router/builder.rb +97 -0
- data/lib/ruby_routes/router/http_helpers.rb +135 -0
- data/lib/ruby_routes/router/resource_helpers.rb +137 -0
- data/lib/ruby_routes/router/scope_helpers.rb +127 -0
- data/lib/ruby_routes/router.rb +196 -182
- data/lib/ruby_routes/segment.rb +28 -8
- data/lib/ruby_routes/segments/base_segment.rb +40 -4
- data/lib/ruby_routes/segments/dynamic_segment.rb +48 -12
- data/lib/ruby_routes/segments/static_segment.rb +43 -7
- data/lib/ruby_routes/segments/wildcard_segment.rb +58 -12
- data/lib/ruby_routes/string_extensions.rb +52 -15
- data/lib/ruby_routes/url_helpers.rb +106 -24
- data/lib/ruby_routes/utility/inflector_utility.rb +35 -0
- data/lib/ruby_routes/utility/key_builder_utility.rb +171 -77
- data/lib/ruby_routes/utility/method_utility.rb +137 -0
- data/lib/ruby_routes/utility/path_utility.rb +75 -28
- data/lib/ruby_routes/utility/route_utility.rb +30 -2
- data/lib/ruby_routes/version.rb +3 -1
- data/lib/ruby_routes.rb +68 -11
- metadata +27 -7
data/lib/ruby_routes/router.rb
CHANGED
@@ -1,210 +1,224 @@
|
|
1
|
-
|
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
|
-
|
8
|
-
|
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(&
|
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
|
-
|
29
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
83
|
+
# Finalize router for DSL immutability.
|
84
|
+
#
|
85
|
+
# @return [Router] self.
|
86
|
+
def finalize!
|
87
|
+
return self if @frozen
|
35
88
|
|
36
|
-
|
37
|
-
|
89
|
+
@frozen = true
|
90
|
+
@scope_stack.freeze
|
91
|
+
@concerns.freeze
|
92
|
+
self
|
38
93
|
end
|
39
94
|
|
40
|
-
#
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
#
|
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(
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
#
|
149
|
-
def
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
#
|
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
|
-
|
168
|
-
|
169
|
-
|
170
|
-
#
|
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
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
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
|
data/lib/ruby_routes/segment.rb
CHANGED
@@ -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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
5
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
5
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
5
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
14
|
-
|
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
|