ruby_routes 2.1.0 → 2.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb6b2a38cb8f31bb3e79fa66dbbb1a0620c6eec1f668680e152efe38e7926b42
4
- data.tar.gz: db5d62093e15a422d85e6c7ec373327ca1f37fd24f7b851c12bdb2f70cf81622
3
+ metadata.gz: 66764348265233b0904630e4193e21fc93f79e3257ec4d0e43e5c533faf1f7e1
4
+ data.tar.gz: de32cc9f5f42398b12e829c4da34aa82afd071fa63ed869180a56203834beecd
5
5
  SHA512:
6
- metadata.gz: 7e84488c6309cea2dab99be9a350139f2042f3d9601e4a0d1d20eb3d66c5530934f2c4f0c5b384dec7fa3095802f4d710ab2cc4f1a0158486cd5bba618995801
7
- data.tar.gz: 0fda0c79e0a935ba9227faba495030c9ff2144ebcc289969eed0a0eb95596d0d69289d1a12b160ef37c18d4021fd5ae172c12b7db48b3beeddb32435e6a2d7ad
6
+ metadata.gz: 21442f1a73e4045fedbb9bd215c639770c55135f6d55c78c17384996515d1347851d7b3afa952db345793a5aa92d5c14368d28faf0e35d4c985d608d94c8b980
7
+ data.tar.gz: 5570f4054ceaf76569bb4f28eca4d94347a559a24febe1936e39dfb9896ccfa281cdd1e2ce24e8e2b157b3d7f3870a67cc9ecdb61b62338bce7e28b5d0c32e4e
@@ -1,65 +1,64 @@
1
1
  require_relative 'segment'
2
2
 
3
3
  module RubyRoutes
4
+ # Node represents a single node in the radix tree structure.
5
+ # Each node can have static children (exact matches), one dynamic child (parameter capture),
6
+ # and one wildcard child (consumes remaining path segments).
4
7
  class Node
5
- attr_accessor :static_children, :dynamic_child, :wildcard_child,
6
- :handlers, :param_name, :is_endpoint
8
+ attr_accessor :param_name, :is_endpoint, :dynamic_child, :wildcard_child
9
+ attr_reader :handlers, :static_children
7
10
 
8
11
  def initialize
12
+ @is_endpoint = false
13
+ @handlers = {}
9
14
  @static_children = {}
10
15
  @dynamic_child = nil
11
16
  @wildcard_child = nil
12
- @handlers = {}
13
17
  @param_name = nil
14
- @is_endpoint = false
15
18
  end
16
19
 
17
- # Fast traversal: minimal allocations, streamlined branching
18
- # Returns [next_node_or_nil, should_break_bool] or [nil, false] if no match.
19
- def traverse_for(segment, index, segments, params)
20
- # Static match: O(1) hash lookup
21
- child = @static_children[segment]
22
- return [child, false] if child
20
+ def add_handler(method, handler)
21
+ method_str = normalize_method(method)
22
+ @handlers[method_str] = handler
23
+ @is_endpoint = true
24
+ end
23
25
 
24
- # Dynamic match: single segment capture
25
- if (dyn = @dynamic_child)
26
- params[dyn.param_name] = segment if params
27
- return [dyn, false]
28
- end
26
+ def get_handler(method)
27
+ @handlers[method]
28
+ end
29
29
 
30
- # Wildcard match: consume remainder (last resort)
31
- if (wild = @wildcard_child)
32
- if params
33
- # Build remainder path without intermediate array allocation
34
- remainder = segments[index..-1]
35
- params[wild.param_name] = remainder.size == 1 ? remainder[0] : remainder.join('/')
30
+ # Fast traversal method with minimal allocations and streamlined branching.
31
+ # Matching order: static (most specific) → dynamic → wildcard (least specific)
32
+ # Returns [next_node_or_nil, should_break_bool] where should_break indicates
33
+ # wildcard capture that consumes remaining path segments.
34
+ def traverse_for(segment, index, segments, params)
35
+ # Try static child first (most specific) - O(1) hash lookup
36
+ if @static_children.key?(segment)
37
+ return [@static_children[segment], false]
38
+ # Try dynamic child (parameter capture) - less specific than static
39
+ elsif @dynamic_child
40
+ # Capture parameter if params hash provided and param_name is set
41
+ params[@dynamic_child.param_name] = segment if params && @dynamic_child.param_name
42
+ return [@dynamic_child, false]
43
+ # Try wildcard child (consumes remaining segments) - least specific
44
+ elsif @wildcard_child
45
+ # Capture remaining path segments for wildcard parameter
46
+ if params && @wildcard_child.param_name
47
+ remaining = segments[index..-1]
48
+ params[@wildcard_child.param_name] = remaining.join('/')
36
49
  end
37
- return [wild, true]
50
+ return [@wildcard_child, true] # true signals to stop traversal
38
51
  end
39
52
 
40
- # No match
53
+ # No match found at this node
41
54
  [nil, false]
42
55
  end
43
56
 
44
- # Pre-cache param names as strings to avoid repeated .to_s calls
45
- def param_name
46
- @param_name_str ||= @param_name&.to_s
47
- end
48
-
49
- def param_name=(name)
50
- @param_name = name
51
- @param_name_str = nil # invalidate cache
52
- end
53
-
54
- # Normalize method once and cache string keys
55
- def add_handler(method, handler)
56
- method_key = method.to_s.upcase
57
- @handlers[method_key] = handler
58
- @is_endpoint = true
59
- end
57
+ private
60
58
 
61
- def get_handler(method)
62
- @handlers[method] # assume already normalized upstream
59
+ # Fast method normalization - converts method to uppercase string
60
+ def normalize_method(method)
61
+ method.to_s.upcase
63
62
  end
64
63
  end
65
64
  end
@@ -1,7 +1,13 @@
1
1
  require_relative 'segment'
2
+ require_relative 'utility/path_utility'
2
3
 
3
4
  module RubyRoutes
5
+ # RadixTree provides an optimized tree structure for fast route matching.
6
+ # Supports static segments, dynamic parameters (:param), and wildcards (*splat).
7
+ # Features longest prefix matching and improved LRU caching for performance.
4
8
  class RadixTree
9
+ include RubyRoutes::Utility::PathUtility
10
+
5
11
  class << self
6
12
  # Allow RadixTree.new(path, options...) to act as a convenience factory
7
13
  def new(*args, &block)
@@ -17,156 +23,239 @@ module RubyRoutes
17
23
  @root = Node.new
18
24
  @split_cache = {}
19
25
  @split_cache_order = []
20
- @split_cache_max = 2048 # larger cache for better hit rates
21
- @empty_segments = [].freeze # reuse for root path
26
+ @split_cache_max = 2048
27
+ @empty_segments = [].freeze
22
28
  end
23
29
 
30
+ # Add a route to the radix tree with specified path, HTTP methods, and handler.
31
+ # Returns the handler for method chaining.
24
32
  def add(path, methods, handler)
25
- current = @root
26
- segments = split_path_raw(path)
33
+ # Normalize path
34
+ normalized_path = normalize_path(path)
35
+
36
+ # Insert into the tree
37
+ insert_route(normalized_path, methods, handler)
38
+
39
+ # Return handler for chaining
40
+ handler
41
+ end
42
+
43
+ # Insert a route into the tree structure, creating nodes as needed.
44
+ # Supports static segments, dynamic parameters (:param), and wildcards (*splat).
45
+ def insert_route(path_str, methods, handler)
46
+ # Skip empty paths
47
+ return handler if path_str.nil? || path_str.empty?
48
+
49
+ path_parts = split_path(path_str)
50
+ current_node = @root
51
+
52
+ # Add path segments to tree
53
+ path_parts.each_with_index do |segment, i|
54
+ if segment.start_with?(':')
55
+ # Dynamic segment (e.g., :id)
56
+ param_name = segment[1..-1]
57
+
58
+ # Create dynamic child if needed
59
+ unless current_node.dynamic_child
60
+ current_node.dynamic_child = Node.new
61
+ current_node.dynamic_child.param_name = param_name
62
+ end
63
+
64
+ current_node = current_node.dynamic_child
65
+ elsif segment.start_with?('*')
66
+ # Wildcard segment (e.g., *path)
67
+ param_name = segment[1..-1]
27
68
 
28
- segments.each do |raw_seg|
29
- seg = RubyRoutes::Segment.for(raw_seg)
30
- current = seg.ensure_child(current)
31
- break if seg.wildcard?
69
+ # Create wildcard child if needed
70
+ unless current_node.wildcard_child
71
+ current_node.wildcard_child = Node.new
72
+ current_node.wildcard_child.param_name = param_name
73
+ end
74
+
75
+ current_node = current_node.wildcard_child
76
+ break # Wildcard consumes the rest of the path
77
+ else
78
+ # Static segment - freeze key for memory efficiency and performance
79
+ segment_key = segment.freeze
80
+ unless current_node.static_children[segment_key]
81
+ current_node.static_children[segment_key] = Node.new
82
+ end
83
+
84
+ current_node = current_node.static_children[segment_key]
85
+ end
86
+ end
87
+
88
+ # Mark node as endpoint and add handler for methods
89
+ current_node.is_endpoint = true
90
+ Array(methods).each do |method|
91
+ method_str = method.to_s.upcase
92
+ current_node.handlers[method_str] = handler
32
93
  end
33
94
 
34
- # Normalize methods once during registration
35
- Array(methods).each { |method| current.add_handler(method.to_s.upcase, handler) }
95
+ handler
36
96
  end
37
97
 
38
- def find(path, method, params_out = nil)
39
- # Handle nil path and method cases
40
- path ||= ''
41
- method = method.to_s.upcase if method
42
- # Strip query string before matching
43
- clean_path = path.split('?', 2).first || ''
44
- # Fast path: root route
45
- if clean_path == '/' || clean_path.empty?
46
- handler = @root.get_handler(method)
47
- if @root.is_endpoint && handler
48
- return [handler, params_out || {}]
98
+ # Find a matching route in the radix tree with longest prefix match support.
99
+ # Tracks the deepest endpoint node during traversal so partial matches return
100
+ # the longest valid prefix, increasing matching flexibility and correctness
101
+ # for overlapping/static/dynamic/wildcard routes.
102
+ def find(path, method, params_out = {})
103
+ # Handle empty path as root
104
+ path_str = path.to_s
105
+ method_str = method.to_s.upcase
106
+
107
+ # Special case for root path
108
+ if path_str.empty? || path_str == '/'
109
+ if @root.is_endpoint && @root.handlers[method_str]
110
+ return [@root.handlers[method_str], params_out || {}]
49
111
  else
50
- return [nil, {}]
112
+ return [nil, params_out || {}]
51
113
  end
52
114
  end
53
115
 
54
- segments = split_path_cached(clean_path)
55
- current = @root
116
+ # Split path into segments
117
+ segments = split_path_cached(path_str)
118
+ return [nil, params_out || {}] if segments.empty?
119
+
56
120
  params = params_out || {}
57
- params.clear if params_out
58
-
59
- # Unrolled traversal for common case (1-3 segments)
60
- case segments.size
61
- when 1
62
- next_node, _ = current.traverse_for(segments[0], 0, segments, params)
63
- current = next_node
64
- when 2
65
- next_node, should_break = current.traverse_for(segments[0], 0, segments, params)
66
- return [nil, {}] unless next_node
67
- current = next_node
68
- unless should_break
69
- next_node, _ = current.traverse_for(segments[1], 1, segments, params)
70
- current = next_node
71
- end
72
- when 3
73
- next_node, should_break = current.traverse_for(segments[0], 0, segments, params)
74
- return [nil, {}] unless next_node
75
- current = next_node
76
- unless should_break
77
- next_node, should_break = current.traverse_for(segments[1], 1, segments, params)
78
- return [nil, {}] unless next_node
79
- current = next_node
80
- unless should_break
81
- next_node, _ = current.traverse_for(segments[2], 2, segments, params)
82
- current = next_node
121
+
122
+ # Track the longest prefix match (deepest endpoint found during traversal)
123
+ # Only consider nodes that actually match some part of the path
124
+ longest_match_node = nil
125
+ longest_match_params = nil
126
+
127
+ # Traverse the tree to find matching route
128
+ current_node = @root
129
+ segments.each_with_index do |segment, i|
130
+ next_node, should_break = current_node.traverse_for(segment, i, segments, params)
131
+
132
+ # No match found for this segment
133
+ unless next_node
134
+ # Return longest prefix match if we found any valid endpoint during traversal
135
+ if longest_match_node
136
+ handler = longest_match_node.handlers[method_str]
137
+ if handler.respond_to?(:constraints)
138
+ constraints = handler.constraints
139
+ if constraints && !constraints.empty?
140
+ return check_constraints(handler, longest_match_params) ? [handler, longest_match_params] : [nil, params]
141
+ end
142
+ end
143
+ return [handler, longest_match_params]
83
144
  end
145
+ return [nil, params]
84
146
  end
85
- else
86
- # General case for longer paths
87
- segments.each_with_index do |text, idx|
88
- next_node, should_break = current.traverse_for(text, idx, segments, params)
89
- return [nil, {}] unless next_node
90
- current = next_node
91
- break if should_break
147
+
148
+ current_node = next_node
149
+
150
+ # Check if current node is a valid endpoint after successful traversal
151
+ if current_node.is_endpoint && current_node.handlers[method_str]
152
+ # Store this as our current best match
153
+ longest_match_node = current_node
154
+ longest_match_params = params.dup
92
155
  end
156
+
157
+ break if should_break # For wildcard paths
93
158
  end
94
159
 
95
- return [nil, {}] unless current
96
- handler = current.get_handler(method)
97
- return [nil, {}] unless current.is_endpoint && handler
160
+ # Check if final node is an endpoint and has a handler for the method
161
+ if current_node.is_endpoint && current_node.handlers[method_str]
162
+ handler = current_node.handlers[method_str]
163
+
164
+ # Handle constraints correctly - only check constraints
165
+ # Don't try to call matches? which test doubles won't have properly stubbed
166
+ if handler.respond_to?(:constraints)
167
+ constraints = handler.constraints
168
+ if constraints && !constraints.empty?
169
+ if check_constraints(handler, params)
170
+ return [handler, params]
171
+ else
172
+ # If constraints fail, try longest prefix match as fallback
173
+ if longest_match_node && longest_match_node != current_node
174
+ fallback_handler = longest_match_node.handlers[method_str]
175
+ return [fallback_handler, longest_match_params] if fallback_handler
176
+ end
177
+ return [nil, params]
178
+ end
179
+ end
180
+ end
98
181
 
99
- # Fast constraint check
100
- if handler.respond_to?(:constraints) && !handler.constraints.empty?
101
- return [nil, {}] unless constraints_match_fast(handler.constraints, params)
182
+ return [handler, params]
102
183
  end
103
184
 
104
- [handler, params]
185
+ # If we reach here, final node isn't an endpoint - return longest prefix match
186
+ if longest_match_node
187
+ handler = longest_match_node.handlers[method_str]
188
+ if handler.respond_to?(:constraints)
189
+ constraints = handler.constraints
190
+ if constraints && !constraints.empty?
191
+ return check_constraints(handler, longest_match_params) ? [handler, longest_match_params] : [nil, params]
192
+ end
193
+ end
194
+ return [handler, longest_match_params]
195
+ end
196
+
197
+ [nil, params]
105
198
  end
106
199
 
107
200
  private
108
201
 
109
- # Cached path splitting with optimized common cases
202
+ # Improved LRU Path Segment Cache: Accessed keys are moved to the end of the
203
+ # order array to ensure proper LRU eviction behavior
110
204
  def split_path_cached(path)
111
- return @empty_segments if path == '/' || path.empty?
205
+ return @empty_segments if path == '/'
112
206
 
113
- if (cached = @split_cache[path])
114
- return cached
207
+ # Check if path is in cache
208
+ if @split_cache.key?(path)
209
+ # Move accessed key to end for proper LRU behavior
210
+ @split_cache_order.delete(path)
211
+ @split_cache_order << path
212
+ return @split_cache[path]
115
213
  end
116
214
 
117
- result = split_path_raw(path)
215
+ # Split path and add to cache
216
+ segments = split_path(path)
118
217
 
119
- # Cache with simple LRU eviction
120
- @split_cache[path] = result
121
- @split_cache_order << path
122
- if @split_cache_order.size > @split_cache_max
123
- oldest = @split_cache_order.shift
124
- @split_cache.delete(oldest)
218
+ # Manage cache size - evict oldest entries when limit reached
219
+ if @split_cache.size >= @split_cache_max
220
+ old_key = @split_cache_order.shift
221
+ @split_cache.delete(old_key)
125
222
  end
126
223
 
127
- result
224
+ @split_cache[path] = segments
225
+ @split_cache_order << path
226
+
227
+ segments
128
228
  end
129
229
 
130
- # Raw path splitting without caching (for registration)
131
- def split_path_raw(path)
132
- return [] if path == '/' || path.empty?
230
+ # Validates route constraints against extracted parameters.
231
+ # Returns true if all constraints pass, false otherwise.
232
+ def check_constraints(handler, params)
233
+ return true unless handler.respond_to?(:constraints)
133
234
 
134
- # Optimized trimming: avoid string allocations when possible
135
- start_idx = path.start_with?('/') ? 1 : 0
136
- end_idx = path.end_with?('/') ? -2 : -1
235
+ constraints = handler.constraints
236
+ return true unless constraints && !constraints.empty?
137
237
 
138
- if start_idx == 0 && end_idx == -1
139
- path.split('/')
140
- else
141
- path[start_idx..end_idx].split('/')
142
- end
143
- end
144
-
145
- # Optimized constraint matching with fast paths
146
- def constraints_match_fast(constraints, params)
238
+ # Check each constraint
147
239
  constraints.each do |param, constraint|
148
- # Try both string and symbol keys (common pattern)
149
- value = params[param.to_s]
150
- value ||= params[param] if param.respond_to?(:to_s)
240
+ param_key = param.to_s
241
+ value = params[param_key]
151
242
  next unless value
152
243
 
153
244
  case constraint
154
245
  when Regexp
155
- return false unless constraint.match?(value)
156
- when Proc
157
- return false unless constraint.call(value)
246
+ return false unless constraint.match?(value.to_s)
158
247
  when :int
159
- # Fast integer check without regex
160
- return false unless value.is_a?(String) && value.match?(/\A\d+\z/)
248
+ return false unless value.to_s.match?(/\A\d+\z/)
161
249
  when :uuid
162
- # Fast UUID check
163
- return false unless value.is_a?(String) && value.length == 36 &&
164
- value.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
165
- when Symbol
166
- # Handle other symbolic constraints
167
- next # unknown symbol constraint — allow
250
+ return false unless value.to_s.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
251
+ when Hash
252
+ if constraint[:range].is_a?(Range)
253
+ value_num = value.to_i
254
+ return false unless constraint[:range].include?(value_num)
255
+ end
168
256
  end
169
257
  end
258
+
170
259
  true
171
260
  end
172
261
  end
@@ -31,6 +31,10 @@ module RubyRoutes
31
31
  val
32
32
  end
33
33
 
34
+ def size
35
+ @h.size
36
+ end
37
+
34
38
  def increment_hits
35
39
  @hits += 1
36
40
  end
@@ -3,23 +3,32 @@ require 'timeout'
3
3
  require 'set'
4
4
  require 'rack'
5
5
  require_relative 'route/small_lru'
6
+ require_relative 'utility/path_utility'
7
+ require_relative 'utility/key_builder_utility'
6
8
 
7
9
  module RubyRoutes
8
10
  class Route
11
+ include RubyRoutes::Utility::PathUtility
12
+ include RubyRoutes::Utility::KeyBuilderUtility
13
+
9
14
  attr_reader :path, :methods, :controller, :action, :name, :constraints, :defaults
10
15
 
16
+ EMPTY_ARRAY = [].freeze
17
+ EMPTY_PAIR = [EMPTY_ARRAY, EMPTY_ARRAY].freeze
18
+ EMPTY_STRING = ''.freeze
19
+
11
20
  def initialize(path, options = {})
12
21
  @path = normalize_path(path)
13
22
  # Pre-normalize and freeze methods at creation time
14
- raw_methods = Array(options[:via] || :get)
15
- @methods = raw_methods.map { |m| normalize_method(m) }.freeze
16
- @methods_set = @methods.to_set.freeze
17
- @controller = extract_controller(options)
18
- @action = options[:action] || extract_action(options[:to])
19
- @name = options[:as]
20
- @constraints = options[:constraints] || {}
23
+ raw_methods = Array(options[:via] || :get)
24
+ @methods = raw_methods.map { |m| normalize_method(m) }.freeze
25
+ @methods_set = @methods.to_set.freeze
26
+ @controller = extract_controller(options)
27
+ @action = options[:action] || extract_action(options[:to])
28
+ @name = options[:as]
29
+ @constraints = options[:constraints] || {}
21
30
  # Pre-normalize defaults to string keys and freeze
22
- @defaults = (options[:defaults] || {}).transform_keys(&:to_s).freeze
31
+ @defaults = (options[:defaults] || {}).transform_keys(&:to_s).freeze
23
32
 
24
33
  # Pre-compile everything at initialization
25
34
  precompile_route_data
@@ -59,35 +68,17 @@ module RubyRoutes
59
68
 
60
69
  # Optimized path generation with better caching and fewer allocations
61
70
  def generate_path(params = {})
62
- return ROOT_PATH if @path == ROOT_PATH
71
+ return @static_path if @static_path && (params.nil? || params.empty?)
72
+ params ||= {}
73
+ missing, nils = validate_required_params(params)
74
+ raise RouteNotFound, "Missing params: #{missing.join(', ')}" unless missing.empty?
75
+ raise RouteNotFound, "Missing or nil params: #{nils.join(', ')}" unless nils.empty?
63
76
 
64
- # Fast path: empty params and no required params
65
- if params.empty? && @required_params.empty?
66
- return @static_path if @static_path
67
- end
68
-
69
- # Build merged params efficiently
70
77
  merged = build_merged_params(params)
71
-
72
- # Check required params (fast Set operation)
73
- missing_params = @required_params_set - merged.keys
74
- unless missing_params.empty?
75
- raise RubyRoutes::RouteNotFound, "Missing params: #{missing_params.to_a.join(', ')}"
76
- end
77
-
78
- # Check for nil values in required params
79
- nil_params = @required_params_set.select { |param| merged[param].nil? }
80
- unless nil_params.empty?
81
- raise RubyRoutes::RouteNotFound, "Missing or nil params: #{nil_params.to_a.join(', ')}"
82
- end
83
-
84
- # Cache lookup
85
- cache_key = build_cache_key_fast(merged)
78
+ cache_key = cache_key_for_params(@required_params, merged)
86
79
  if (cached = @gen_cache.get(cache_key))
87
80
  return cached
88
81
  end
89
-
90
- # Generate path using string buffer (avoid array allocations)
91
82
  path_str = generate_path_string(merged)
92
83
  @gen_cache.set(cache_key, path_str)
93
84
  path_str
@@ -134,6 +125,7 @@ module RubyRoutes
134
125
  @is_resource = @path.match?(/\/:id(?:$|\.)/)
135
126
  @gen_cache = SmallLru.new(512) # larger cache
136
127
  @query_cache = SmallLru.new(QUERY_CACHE_SIZE)
128
+ initialize_validation_cache
137
129
 
138
130
  compile_segments
139
131
  compile_required_params
@@ -193,7 +185,7 @@ module RubyRoutes
193
185
  # Validate constraints efficiently
194
186
  validate_constraints_fast!(result) unless @constraints.empty?
195
187
 
196
- result.dup
188
+ result
197
189
  end
198
190
 
199
191
  def get_thread_local_hash
@@ -222,7 +214,7 @@ module RubyRoutes
222
214
  return EMPTY_HASH if @compiled_segments.empty? && request_path == ROOT_PATH
223
215
  return nil if @compiled_segments.empty?
224
216
 
225
- path_parts = split_path_fast(request_path)
217
+ path_parts = split_path(request_path)
226
218
 
227
219
  # Check for wildcard/splat segment
228
220
  has_splat = @compiled_segments.any? { |seg| seg[:type] == :splat }
@@ -236,14 +228,6 @@ module RubyRoutes
236
228
  extract_params_from_parts(path_parts)
237
229
  end
238
230
 
239
- def split_path_fast(request_path)
240
- # Remove query string before splitting
241
- path = request_path.split('?', 2).first
242
- path = path[1..-1] if path.start_with?('/')
243
- path = path[0...-1] if path.end_with?('/') && path != ROOT_PATH
244
- path.empty? ? [] : path.split('/')
245
- end
246
-
247
231
  def extract_params_from_parts(path_parts)
248
232
  params = {}
249
233
 
@@ -264,22 +248,16 @@ module RubyRoutes
264
248
 
265
249
  # Optimized merged params building
266
250
  def build_merged_params(params)
267
- return @defaults if params.empty?
268
-
269
- merged = get_thread_local_merged_hash
270
-
271
- # Merge defaults first if they exist
272
- merged.update(@defaults) unless @defaults.empty?
273
-
274
- # Use merge! with transform_keys for better performance
275
- if params.respond_to?(:transform_keys)
276
- merged.merge!(params.transform_keys(&:to_s))
277
- else
278
- # Fallback for older Ruby versions
279
- params.each { |k, v| merged[k.to_s] = v }
251
+ return @defaults if params.nil? || params.empty?
252
+ h = Thread.current[:ruby_routes_merge_hash] ||= {}
253
+ h.clear
254
+ @defaults.each { |k,v| h[k] = v }
255
+ params.each do |k,v|
256
+ next if v.nil?
257
+ ks = k.is_a?(String) ? k : k.to_s
258
+ h[ks] = v
280
259
  end
281
-
282
- merged
260
+ h
283
261
  end
284
262
 
285
263
  def get_thread_local_merged_hash
@@ -288,41 +266,43 @@ module RubyRoutes
288
266
  hash
289
267
  end
290
268
 
291
- # Fast cache key building with minimal allocations
292
- def build_cache_key_fast(merged)
293
- return '' if @required_params.empty?
294
-
295
- # Use array join which is faster than string concatenation
296
- parts = @required_params.map do |name|
297
- value = merged[name]
298
- value.is_a?(Array) ? value.join('/') : value.to_s
299
- end
300
- parts.join('|')
301
- end
302
-
303
269
  # Optimized path generation
304
270
  def generate_path_string(merged)
305
271
  return ROOT_PATH if @compiled_segments.empty?
306
272
 
307
- # Pre-allocate array for parts to avoid string buffer operations
308
- parts = []
309
-
273
+ # Estimate final path length to avoid resizing
274
+ estimated_size = 1 # For leading slash
310
275
  @compiled_segments.each do |seg|
311
276
  case seg[:type]
312
277
  when :static
313
- parts << seg[:value]
278
+ estimated_size += seg[:value].length + 1 # +1 for slash
279
+ when :param, :splat
280
+ estimated_size += 20 # Average param length estimate
281
+ end
282
+ end
283
+
284
+ # Use string buffer with pre-allocated capacity
285
+ path = String.new(capacity: estimated_size)
286
+ path << '/'
287
+
288
+ # Generate path directly into buffer
289
+ last_idx = @compiled_segments.size - 1
290
+ @compiled_segments.each_with_index do |seg, i|
291
+ case seg[:type]
292
+ when :static
293
+ path << seg[:value]
314
294
  when :param
315
295
  value = merged.fetch(seg[:name]).to_s
316
- parts << encode_segment_fast(value)
296
+ path << encode_segment_fast(value)
317
297
  when :splat
318
298
  value = merged.fetch(seg[:name], '')
319
- parts << format_splat_value(value)
299
+ path << format_splat_value(value)
320
300
  end
301
+
302
+ path << '/' unless i == last_idx
321
303
  end
322
304
 
323
- # Single join operation is faster than multiple string concatenations
324
- path = "/#{parts.join('/')}"
325
- path == '/' ? ROOT_PATH : path
305
+ path
326
306
  end
327
307
 
328
308
  def format_splat_value(value)
@@ -366,13 +346,6 @@ module RubyRoutes
366
346
  result
367
347
  end
368
348
 
369
- def normalize_path(path)
370
- path_str = path.to_s
371
- path_str = "/#{path_str}" unless path_str.start_with?('/')
372
- path_str = path_str.chomp('/') unless path_str == ROOT_PATH
373
- path_str
374
- end
375
-
376
349
  def extract_controller(options)
377
350
  to = options[:to]
378
351
  return options[:controller] unless to
@@ -384,6 +357,84 @@ module RubyRoutes
384
357
  to.to_s.split('#', 2).last
385
358
  end
386
359
 
360
+
361
+ def validate_required_params(params)
362
+ return EMPTY_PAIR if @required_params.empty?
363
+ missing = nil
364
+ nils = nil
365
+ @required_params.each do |rk|
366
+ if params.key?(rk)
367
+ (nils ||= []) << rk if params[rk].nil?
368
+ elsif params.key?(sym = rk.to_sym)
369
+ (nils ||= []) << rk if params[sym].nil?
370
+ else
371
+ (missing ||= []) << rk
372
+ end
373
+ end
374
+ [missing || EMPTY_ARRAY, nils || EMPTY_ARRAY]
375
+ end
376
+
377
+ # Add this validation cache management
378
+ def initialize_validation_cache
379
+ @validation_cache = SmallLru.new(64)
380
+ end
381
+
382
+ def cache_validation_result(params, result)
383
+ # Only cache immutable params to prevent subtle bugs
384
+ if params.frozen? && @validation_cache && @validation_cache.size < 64
385
+ @validation_cache.set(params.hash, result)
386
+ end
387
+ end
388
+
389
+ def get_cached_validation(params)
390
+ return nil unless @validation_cache
391
+ @validation_cache.get(params.hash)
392
+ end
393
+
394
+ # Fast parameter type detection with result caching
395
+ def params_type(params)
396
+ # Cache parameter type detection results
397
+ @params_type_cache ||= {}
398
+ param_obj_id = params.object_id
399
+
400
+ # Return cached type if available
401
+ return @params_type_cache[param_obj_id] if @params_type_cache.key?(param_obj_id)
402
+
403
+ # Type detection with explicit checks
404
+ type = if params.is_a?(Hash)
405
+ # Further refine hash type for optimization
406
+ refine_hash_type(params)
407
+ elsif params.respond_to?(:each) && params.respond_to?(:[])
408
+ # Hash-like enumerable
409
+ :enumerable
410
+ else
411
+ # Last resort - may
412
+ :mehod_missing
413
+ end
414
+
415
+ # Keep cache small
416
+ if @params_type_cache.size > 100
417
+ @params_type_cache.clear
418
+ end
419
+
420
+ # Cache result
421
+ @params_type_cache[param_obj_id] = type
422
+ end
423
+
424
+ # Further refine hash type for potential optimization
425
+ def refine_hash_type(params)
426
+ # Only sample a few keys to determine type tendency
427
+ key_samples = params.keys.take(3)
428
+
429
+ if key_samples.all? { |k| k.is_a?(String) }
430
+ :string_keyed_hash
431
+ elsif key_samples.all? { |k| k.is_a?(Symbol) }
432
+ :symbol_keyed_hash
433
+ else
434
+ :hash # Mixed keys
435
+ end
436
+ end
437
+
387
438
  # Optimized constraint validation
388
439
  def validate_constraints_fast!(params)
389
440
  @constraints.each do |param, constraint|
@@ -497,9 +548,5 @@ module RubyRoutes
497
548
  raise InvalidRoute, "Action is required" if @action.nil?
498
549
  raise InvalidRoute, "Invalid HTTP method: #{@methods}" if @methods.empty?
499
550
  end
500
-
501
- # Additional constants
502
- EMPTY_ARRAY = [].freeze
503
- EMPTY_STRING = ''.freeze
504
551
  end
505
552
  end
@@ -1,94 +1,103 @@
1
+ require_relative 'utility/key_builder_utility'
2
+
1
3
  module RubyRoutes
2
4
  class RouteSet
3
5
  attr_reader :routes
4
6
 
7
+ include RubyRoutes::Utility::KeyBuilderUtility
8
+
5
9
  def initialize
6
- @tree = RubyRoutes::RadixTree.new
7
- @named_routes = {}
8
10
  @routes = []
9
- # Optimized recognition cache with better data structures
11
+ @named_routes = {}
10
12
  @recognition_cache = {}
13
+ @recognition_cache_max = 2048
11
14
  @cache_hits = 0
12
15
  @cache_misses = 0
13
- @recognition_cache_max = 8192 # larger for better hit rates
16
+ @radix_tree = RadixTree.new
14
17
  end
15
18
 
16
- def add_route(route)
19
+ def add_to_collection(route)
17
20
  @routes << route
18
- @tree.add(route.path, route.methods, route)
21
+ @radix_tree.add(route.path, route.methods, route)
19
22
  @named_routes[route.name] = route if route.named?
20
- # Clear recognition cache when routes change
21
- @recognition_cache.clear if @recognition_cache.size > 100
22
- route
23
23
  end
24
24
 
25
- def find_route(request_method, request_path)
26
- # Optimized: avoid repeated string allocation
27
- method_up = request_method.to_s.upcase
28
- handler, _params = @tree.find(request_path, method_up)
29
- handler
25
+ alias_method :add_route, :add_to_collection
26
+
27
+ def find_route(method, path)
28
+ route, _ = @radix_tree.find(path, method)
29
+ route
30
30
  end
31
31
 
32
32
  def find_named_route(name)
33
33
  route = @named_routes[name]
34
- return route if route
35
- raise RouteNotFound, "No route named '#{name}'"
34
+ raise RouteNotFound.new("No route named '#{name}'") unless route
35
+ route
36
36
  end
37
37
 
38
- def match(request_method, request_path)
39
- # Fast path: normalize method once
40
- method_up = method_lookup(request_method)
38
+ FAST_METHOD_MAP = {
39
+ get: 'GET', post: 'POST', put: 'PUT', patch: 'PATCH',
40
+ delete: 'DELETE', head: 'HEAD', options: 'OPTIONS'
41
+ }.freeze
42
+
43
+ def normalize_method_input(method)
44
+ case method
45
+ when Symbol
46
+ FAST_METHOD_MAP[method] || method.to_s.upcase
47
+ when String
48
+ # Fast path: assume already correct; fallback only for common lowercase
49
+ return method if method.length <= 6 && method == method.upcase
50
+ FAST_METHOD_MAP[method.downcase.to_sym] || method.upcase
51
+ else
52
+ s = method.to_s
53
+ FAST_METHOD_MAP[s.downcase.to_sym] || s.upcase
54
+ end
55
+ end
56
+ private :normalize_method_input
41
57
 
42
- # Optimized cache key: avoid string interpolation when possible
43
- cache_key = build_cache_key(method_up, request_path)
58
+ def match(method, path)
59
+ m = normalize_method_input(method)
60
+ raw = path.to_s
61
+ cache_key = cache_key_for_request(m, raw)
44
62
 
45
- # Cache hit: return immediately (cached result includes full structure)
46
- if (cached_result = @recognition_cache[cache_key])
63
+ # Single cache lookup with proper hit accounting
64
+ if (hit = @recognition_cache[cache_key])
47
65
  @cache_hits += 1
48
- return cached_result
66
+ return hit
49
67
  end
50
68
 
51
69
  @cache_misses += 1
52
70
 
53
- # Use thread-local params to avoid allocations
54
- params = get_thread_local_params
55
- handler, _ = @tree.find(request_path, method_up, params)
56
- return nil unless handler
71
+ path_without_query, _qs = raw.split('?', 2)
57
72
 
58
- route = handler
73
+ # Use normalized method (m) for trie lookup
74
+ route, params = @radix_tree.find(path_without_query, m)
75
+ return nil unless route
59
76
 
60
- # Fast path: merge defaults only if they exist
61
- merge_defaults(route, params) if route.defaults && !route.defaults.empty?
77
+ merge_query_params(route, raw, params)
62
78
 
63
- # Fast path: parse query params only if needed
64
- if request_path.include?('?')
65
- merge_query_params(route, request_path, params)
79
+ if route.respond_to?(:defaults) && route.defaults
80
+ route.defaults.each { |k,v| params[k.to_s] = v unless params.key?(k.to_s) }
66
81
  end
67
82
 
68
- # Create return hash and cache the complete result
69
- result_params = params.dup
70
83
  result = {
71
84
  route: route,
72
- params: result_params,
85
+ params: params,
73
86
  controller: route.controller,
74
87
  action: route.action
75
- }.freeze
88
+ }
76
89
 
77
90
  insert_cache_entry(cache_key, result)
78
91
  result
79
92
  end
80
93
 
81
- def recognize_path(path, method = :get)
94
+ def recognize_path(path, method = 'GET')
82
95
  match(method, path)
83
96
  end
84
97
 
85
98
  def generate_path(name, params = {})
86
- route = @named_routes[name]
87
- if route
88
- route.generate_path(params)
89
- else
90
- raise RouteNotFound, "No route named '#{name}'"
91
- end
99
+ route = find_named_route(name)
100
+ route.generate_path(params)
92
101
  end
93
102
 
94
103
  def generate_path_from_route(route, params = {})
@@ -99,94 +108,76 @@ module RubyRoutes
99
108
  @routes.clear
100
109
  @named_routes.clear
101
110
  @recognition_cache.clear
102
- @tree = RadixTree.new
103
- @cache_hits = @cache_misses = 0
111
+ @cache_hits = 0
112
+ @cache_misses = 0
113
+ # Create a new radix tree since we can't clear it
114
+ @radix_tree = RadixTree.new
104
115
  end
105
116
 
106
117
  def size
107
118
  @routes.size
108
119
  end
109
- alias_method :length, :size
110
120
 
111
121
  def empty?
112
122
  @routes.empty?
113
123
  end
114
124
 
115
- def each(&block)
116
- return enum_for(:each) unless block_given?
117
- @routes.each(&block)
118
- end
119
-
120
- def include?(route)
121
- @routes.include?(route)
122
- end
123
-
124
- # Performance monitoring
125
125
  def cache_stats
126
- total = @cache_hits + @cache_misses
127
- hit_rate = total > 0 ? (@cache_hits.to_f / total * 100).round(2) : 0
126
+ lookups = @cache_hits + @cache_misses
128
127
  {
129
128
  hits: @cache_hits,
130
129
  misses: @cache_misses,
131
- hit_rate: "#{hit_rate}%",
130
+ hit_rate: lookups.zero? ? 0.0 : (@cache_hits.to_f / lookups * 100.0),
132
131
  size: @recognition_cache.size
133
132
  }
134
133
  end
135
134
 
136
- private
137
-
138
- # Method lookup table to avoid repeated upcasing with interned strings
139
- def method_lookup(method)
140
- @method_cache ||= Hash.new { |h, k| h[k] = k.to_s.upcase.freeze }
141
- @method_cache[method]
142
- end
143
-
144
- # Optimized cache key building - avoid string interpolation
145
- def build_cache_key(method, path)
146
- # String interpolation creates a new string directly without intermediate allocations
147
- "#{method}:#{path}"
135
+ def each(&block)
136
+ @routes.each(&block)
148
137
  end
149
138
 
150
- # Get thread-local params hash, reusing when possible
151
- def get_thread_local_params
152
- # Use single thread-local hash that gets cleared, avoiding pool management overhead
153
- hash = Thread.current[:ruby_routes_params_hash] ||= {}
154
- hash.clear
155
- hash
139
+ def include?(route)
140
+ @routes.include?(route)
156
141
  end
157
142
 
158
- def return_params_to_pool(params)
159
- # No-op since we're using a single reusable hash per thread
160
- end
143
+ private
161
144
 
162
- # Fast defaults merging
163
- def merge_defaults(route, params)
164
- route.defaults.each do |key, value|
165
- params[key] = value unless params.key?(key)
145
+ def insert_cache_entry(key, value)
146
+ # unchanged cache insert (key already frozen & reusable)
147
+ if @recognition_cache.size >= @recognition_cache_max
148
+ @recognition_cache.keys.first(@recognition_cache_max / 4).each { |k| @recognition_cache.delete(k) }
149
+ end
150
+ @recognition_cache[key] = value
151
+ end
152
+
153
+ # Add the missing method for merging query params
154
+ def merge_query_params(route, path, params)
155
+ # Check for query string
156
+ if path.to_s.include?('?')
157
+ if route.respond_to?(:parse_query_params)
158
+ query_params = route.parse_query_params(path)
159
+ params.merge!(query_params) if query_params
160
+ elsif route.respond_to?(:query_params)
161
+ query_params = route.query_params(path)
162
+ params.merge!(query_params) if query_params
163
+ end
166
164
  end
167
165
  end
168
166
 
169
- # Fast query params merging
170
- def merge_query_params(route, request_path, params)
171
- if route.respond_to?(:parse_query_params)
172
- qp = route.parse_query_params(request_path)
173
- params.merge!(qp) unless qp.empty?
174
- elsif route.respond_to?(:query_params)
175
- qp = route.query_params(request_path)
176
- params.merge!(qp) unless qp.empty?
167
+ # Add thread-local params pool methods
168
+ def get_thread_local_params
169
+ thread_params = Thread.current[:ruby_routes_params_pool] ||= []
170
+ if thread_params.empty?
171
+ {}
172
+ else
173
+ thread_params.pop.clear
177
174
  end
178
175
  end
179
176
 
180
- # Efficient cache insertion with LRU eviction
181
- def insert_cache_entry(cache_key, cache_entry)
182
- @recognition_cache[cache_key] = cache_entry
183
-
184
- # Simple eviction: clear cache when it gets too large
185
- if @recognition_cache.size > @recognition_cache_max
186
- # Keep most recently used half
187
- keys_to_delete = @recognition_cache.keys[0...(@recognition_cache_max / 2)]
188
- keys_to_delete.each { |k| @recognition_cache.delete(k) }
189
- end
177
+ def return_params_to_pool(params)
178
+ params.clear
179
+ thread_pool = Thread.current[:ruby_routes_params_pool] ||= []
180
+ thread_pool << params if thread_pool.size < 10 # Limit pool size
190
181
  end
191
182
  end
192
183
  end
@@ -1,9 +1,12 @@
1
+ require_relative 'utility/route_utility'
2
+
1
3
  module RubyRoutes
2
4
  class Router
3
5
  attr_reader :route_set
4
6
 
5
7
  def initialize(&block)
6
8
  @route_set = RouteSet.new
9
+ @route_utils = RubyRoutes::Utility::RouteUtility.new(@route_set)
7
10
  @scope_stack = []
8
11
  @concerns = {}
9
12
  instance_eval(&block) if block_given?
@@ -102,7 +105,14 @@ module RubyRoutes
102
105
  end
103
106
 
104
107
  # Scope support
105
- def scope(options = {}, &block)
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
+
106
116
  @scope_stack.push(options)
107
117
 
108
118
  if block_given?
@@ -165,14 +175,9 @@ module RubyRoutes
165
175
 
166
176
  private
167
177
 
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
178
+ def add_route(path, options={})
179
+ scoped = apply_scope(path, options)
180
+ @route_utils.define(scoped[:path], scoped)
176
181
  end
177
182
 
178
183
  def apply_scope(path, options)
@@ -0,0 +1,102 @@
1
+ module RubyRoutes
2
+ module Utility
3
+ module KeyBuilderUtility
4
+ # ------------------------------------------------------------------
5
+ # Fast reusable key storage for "METHOD:PATH" strings.
6
+ # O(1) insert + O(1) eviction using a fixed-size ring buffer.
7
+ # Only allocates a new String on the *first* time a (method,path) pair appears.
8
+ # ------------------------------------------------------------------
9
+ REQUEST_KEY_CAPACITY = 4096
10
+
11
+ @pool = {} # { method_string => { path_string => frozen_key_string } }
12
+ @ring = Array.new(REQUEST_KEY_CAPACITY) # ring entries: [method_string, path_string]
13
+ @ring_pos = 0
14
+ @entry_cnt = 0
15
+
16
+ class << self
17
+ attr_reader :pool
18
+ def fetch_request_key(method, path)
19
+ # Method & path must be strings already (callers ensure)
20
+ if (paths = @pool[method])
21
+ if (key = paths[path])
22
+ return key
23
+ end
24
+ end
25
+
26
+ # MISS: build & freeze once
27
+ key = "#{method}:#{path}".freeze
28
+ if paths
29
+ paths[path] = key
30
+ else
31
+ @pool[method] = { path => key }
32
+ end
33
+
34
+ # Evict if ring full (overwrite oldest slot)
35
+ if @entry_cnt < REQUEST_KEY_CAPACITY
36
+ @ring[@entry_cnt] = [method, path]
37
+ @entry_cnt += 1
38
+ else
39
+ ev_m, ev_p = @ring[@ring_pos]
40
+ bucket = @pool[ev_m]
41
+ if bucket&.delete(ev_p) && bucket.empty?
42
+ @pool.delete(ev_m)
43
+ end
44
+ @ring[@ring_pos] = [method, path]
45
+ @ring_pos += 1
46
+ @ring_pos = 0 if @ring_pos == REQUEST_KEY_CAPACITY
47
+ end
48
+
49
+ key
50
+ end
51
+ end
52
+
53
+ # ------------------------------------------------------------------
54
+ # Public helpers mixed into instances
55
+ # ------------------------------------------------------------------
56
+
57
+ # Generic key (rarely hot): joins parts with delim; single allocation.
58
+ def build_key(parts, delim = ':')
59
+ return ''.freeze if parts.empty?
60
+ buf = Thread.current[:ruby_routes_key_buf] ||= String.new
61
+ buf.clear
62
+ i = 0
63
+ while i < parts.length
64
+ buf << delim unless i.zero?
65
+ buf << parts[i].to_s
66
+ i += 1
67
+ end
68
+ buf.dup
69
+ end
70
+
71
+ # HOT: request cache key (reused frozen interned string)
72
+ def cache_key_for_request(method, path)
73
+ KeyBuilderUtility.fetch_request_key(method, path.to_s)
74
+ end
75
+
76
+ # HOT: params key – produces a short-lived String (dup, not re-frozen each time).
77
+ # Callers usually put it into an LRU that duplicates again, so keep it lean.
78
+ def cache_key_for_params(required_params, merged)
79
+ return ''.freeze if required_params.nil? || required_params.empty?
80
+ buf = Thread.current[:ruby_routes_param_key_buf] ||= String.new
81
+ buf.clear
82
+ i = 0
83
+ while i < required_params.length
84
+ buf << '|' unless i.zero?
85
+ v = merged[required_params[i]]
86
+ if v.is_a?(Array)
87
+ j = 0
88
+ while j < v.length
89
+ buf << '/' unless j.zero?
90
+ buf << v[j].to_s
91
+ j += 1
92
+ end
93
+ else
94
+ buf << v.to_s
95
+ end
96
+ i += 1
97
+ end
98
+ buf.dup
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,42 @@
1
+ module RubyRoutes
2
+ module Utility
3
+ module PathUtility
4
+ ROOT_PATH = '/'.freeze
5
+
6
+ def normalize_path(path)
7
+ path = path.to_s
8
+ # Add leading slash if missing
9
+ path = '/' + path unless path.start_with?('/')
10
+ # Remove trailing slash if present (unless root)
11
+ path = path[0..-2] if path.length > 1 && path.end_with?('/')
12
+ path
13
+ end
14
+
15
+ def split_path(path)
16
+ # Remove query string before splitting
17
+ path = path.split('?', 2).first
18
+ path = path[1..-1] if path.start_with?('/')
19
+ path = path[0...-1] if path.end_with?('/') && path != ROOT_PATH
20
+ path.empty? ? [] : path.split('/')
21
+ end
22
+
23
+ def join_path_parts(parts)
24
+ # Pre-calculate the size to avoid buffer resizing
25
+ size = parts.sum { |p| p.length + 1 } # +1 for slash
26
+
27
+ # Use string buffer for better performance
28
+ result = String.new(capacity: size)
29
+ result << '/'
30
+
31
+ # Join with explicit concatenation rather than array join
32
+ last_idx = parts.size - 1
33
+ parts.each_with_index do |part, i|
34
+ result << part
35
+ result << '/' unless i == last_idx
36
+ end
37
+
38
+ result
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ module RubyRoutes
2
+ module Utility
3
+ class RouteUtility
4
+ def initialize(route_set)
5
+ @route_set = route_set
6
+ end
7
+
8
+ # DSL wants to merge scope, RouteSet wants to add a pre‐built Route,
9
+ # so we offer two entry points:
10
+ def define(path, options = {})
11
+ route = Route.new(path, options)
12
+ register(route)
13
+ end
14
+
15
+ def register(route)
16
+ @route_set.add_to_collection(route)
17
+ route
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,3 @@
1
1
  module RubyRoutes
2
- VERSION = "2.1.0"
2
+ VERSION = "2.2.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_routes
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yosef Benny Widyokarsono
@@ -92,6 +92,9 @@ files:
92
92
  - lib/ruby_routes/segments/wildcard_segment.rb
93
93
  - lib/ruby_routes/string_extensions.rb
94
94
  - lib/ruby_routes/url_helpers.rb
95
+ - lib/ruby_routes/utility/key_builder_utility.rb
96
+ - lib/ruby_routes/utility/path_utility.rb
97
+ - lib/ruby_routes/utility/route_utility.rb
95
98
  - lib/ruby_routes/version.rb
96
99
  homepage: https://github.com/yosefbennywidyo/ruby_routes
97
100
  licenses: