ruby_routes 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +240 -163
  3. data/lib/ruby_routes/constant.rb +137 -18
  4. data/lib/ruby_routes/lru_strategies/hit_strategy.rb +31 -4
  5. data/lib/ruby_routes/lru_strategies/miss_strategy.rb +21 -0
  6. data/lib/ruby_routes/node.rb +86 -36
  7. data/lib/ruby_routes/radix_tree/finder.rb +213 -0
  8. data/lib/ruby_routes/radix_tree/inserter.rb +96 -0
  9. data/lib/ruby_routes/radix_tree.rb +65 -230
  10. data/lib/ruby_routes/route/check_helpers.rb +115 -0
  11. data/lib/ruby_routes/route/constraint_validator.rb +173 -0
  12. data/lib/ruby_routes/route/param_support.rb +200 -0
  13. data/lib/ruby_routes/route/path_builder.rb +84 -0
  14. data/lib/ruby_routes/route/path_generation.rb +87 -0
  15. data/lib/ruby_routes/route/query_helpers.rb +56 -0
  16. data/lib/ruby_routes/route/segment_compiler.rb +166 -0
  17. data/lib/ruby_routes/route/small_lru.rb +93 -18
  18. data/lib/ruby_routes/route/validation_helpers.rb +174 -0
  19. data/lib/ruby_routes/route/warning_helpers.rb +57 -0
  20. data/lib/ruby_routes/route.rb +127 -501
  21. data/lib/ruby_routes/route_set/cache_helpers.rb +76 -0
  22. data/lib/ruby_routes/route_set/collection_helpers.rb +125 -0
  23. data/lib/ruby_routes/route_set.rb +140 -132
  24. data/lib/ruby_routes/router/build_helpers.rb +99 -0
  25. data/lib/ruby_routes/router/builder.rb +97 -0
  26. data/lib/ruby_routes/router/http_helpers.rb +135 -0
  27. data/lib/ruby_routes/router/resource_helpers.rb +137 -0
  28. data/lib/ruby_routes/router/scope_helpers.rb +127 -0
  29. data/lib/ruby_routes/router.rb +196 -182
  30. data/lib/ruby_routes/segment.rb +28 -8
  31. data/lib/ruby_routes/segments/base_segment.rb +40 -4
  32. data/lib/ruby_routes/segments/dynamic_segment.rb +48 -12
  33. data/lib/ruby_routes/segments/static_segment.rb +43 -7
  34. data/lib/ruby_routes/segments/wildcard_segment.rb +58 -12
  35. data/lib/ruby_routes/string_extensions.rb +52 -15
  36. data/lib/ruby_routes/url_helpers.rb +106 -24
  37. data/lib/ruby_routes/utility/inflector_utility.rb +35 -0
  38. data/lib/ruby_routes/utility/key_builder_utility.rb +171 -77
  39. data/lib/ruby_routes/utility/method_utility.rb +137 -0
  40. data/lib/ruby_routes/utility/path_utility.rb +75 -28
  41. data/lib/ruby_routes/utility/route_utility.rb +30 -2
  42. data/lib/ruby_routes/version.rb +3 -1
  43. data/lib/ruby_routes.rb +68 -11
  44. metadata +27 -7
@@ -1,552 +1,178 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
  require 'timeout'
3
- require 'set'
4
5
  require 'rack'
6
+ require 'set'
7
+ require_relative 'constant'
8
+ require_relative 'node'
5
9
  require_relative 'route/small_lru'
6
- require_relative 'utility/path_utility'
7
10
  require_relative 'utility/key_builder_utility'
11
+ require_relative 'utility/method_utility'
12
+ require_relative 'utility/path_utility'
13
+ require_relative 'utility/route_utility'
14
+ require_relative 'route/param_support'
15
+ require_relative 'route/segment_compiler'
16
+ require_relative 'route/path_builder'
17
+ require_relative 'route/constraint_validator'
18
+ require_relative 'route/check_helpers'
19
+ require_relative 'route/query_helpers'
20
+ require_relative 'route/validation_helpers'
21
+ require_relative 'route/path_generation'
8
22
 
9
23
  module RubyRoutes
24
+ # Route
25
+ #
26
+ # Immutable-ish representation of a single HTTP route plus optimized
27
+ # helpers for:
28
+ # - Path recognition (segment compilation + fast param extraction)
29
+ # - Path generation (low‑allocation caching + param merging)
30
+ # - Constraint validation (regexp / typed / hash rules)
31
+ #
32
+ # Performance Techniques:
33
+ # - Precompiled segment descriptors (static / param / splat)
34
+ # - Small LRU caches (path generation + query parsing)
35
+ # - Thread‑local reusable Hashes / String buffers
36
+ # - Minimal object allocation in hot paths
37
+ #
38
+ # Thread Safety:
39
+ # - Instance is effectively read‑only after initialization.
40
+ # - Internal caches (@query_cache, @gen_cache, @validation_cache) are protected
41
+ # by a mutex for safe concurrent access across multiple threads.
42
+ # - Designed for "build during boot, read per request" usage pattern.
43
+ #
44
+ # Public API Surface (stable):
45
+ # - #match?
46
+ # - #extract_params
47
+ # - #generate_path
48
+ # - #named?
49
+ # - #resource? / #collection?
50
+ #
51
+ # @api public
10
52
  class Route
53
+ include ParamSupport
54
+ include SegmentCompiler
55
+ include PathBuilder
56
+ include RubyRoutes::Route::ConstraintValidator
57
+ include RubyRoutes::Route::ValidationHelpers
58
+ include RubyRoutes::Route::QueryHelpers
59
+ include RubyRoutes::Route::PathGeneration
60
+ include RubyRoutes::Utility::MethodUtility
11
61
  include RubyRoutes::Utility::PathUtility
12
62
  include RubyRoutes::Utility::KeyBuilderUtility
13
63
 
14
64
  attr_reader :path, :methods, :controller, :action, :name, :constraints, :defaults
15
65
 
16
- EMPTY_ARRAY = [].freeze
17
- EMPTY_PAIR = [EMPTY_ARRAY, EMPTY_ARRAY].freeze
18
- EMPTY_STRING = ''.freeze
19
-
66
+ public :extract_params, :parse_query_params, :query_params, :generate_path
67
+
68
+ # Create a new Route.
69
+ #
70
+ # @param path [String] The raw route path (may include `:params` or `*splat`).
71
+ # @param options [Hash] The options for the route.
72
+ # @option options [Symbol, String, Array<Symbol, String>] :via (:get) HTTP method(s).
73
+ # @option options [String] :to ("controller#action") The controller and action.
74
+ # @option options [String] :controller Explicit controller (overrides `:to`).
75
+ # @option options [String, Symbol] :action Explicit action (overrides part after `#`).
76
+ # @option options [Hash] :constraints Parameter constraints (Regexp / Symbol / Hash).
77
+ # @option options [Hash] :defaults Default parameter values.
78
+ # @option options [Symbol, String] :as The route name.
20
79
  def initialize(path, options = {})
21
80
  @path = normalize_path(path)
22
- # Pre-normalize and freeze methods at creation time
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] || {}
30
- # Pre-normalize defaults to string keys and freeze
31
- @defaults = (options[:defaults] || {}).transform_keys(&:to_s).freeze
32
81
 
33
- # Pre-compile everything at initialization
82
+ setup_methods(options)
83
+ setup_controller_and_action(options)
84
+
85
+ @name = options[:as]
86
+ @constraints = options[:constraints] || {}
87
+ @defaults = (options[:defaults] || {}).transform_keys(&:to_s).freeze
88
+ @param_key_slots = [[nil, nil], [nil, nil]]
89
+ @required_validated_once = false
90
+
34
91
  precompile_route_data
35
92
  validate_route!
36
93
  end
37
94
 
95
+ # Test if this route matches an HTTP method + path string.
96
+ #
97
+ # @param request_method [String, Symbol] The HTTP method.
98
+ # @param request_path [String] The request path.
99
+ # @return [Boolean] `true` if the route matches, `false` otherwise.
38
100
  def match?(request_method, request_path)
39
- # Fast method check: use frozen Set for O(1) lookup
40
- return false unless @methods_set.include?(request_method.to_s.upcase)
41
- !!extract_path_params_fast(request_path)
42
- end
43
-
44
- def extract_params(request_path, parsed_qp = nil)
45
- path_params = extract_path_params_fast(request_path)
101
+ normalized_method = normalize_http_method(request_method)
102
+ return false unless @methods_set.include?(normalized_method)
46
103
 
47
- return EMPTY_HASH unless path_params
48
-
49
- # Use optimized param building
50
- build_params_hash(path_params, request_path, parsed_qp)
104
+ !!extract_path_params_fast(request_path)
51
105
  end
52
106
 
107
+ # @return [Boolean] Whether this route has a name.
53
108
  def named?
54
109
  !@name.nil?
55
110
  end
56
111
 
112
+ # @return [Boolean] Heuristic: path contains `:id` implying a resource member.
57
113
  def resource?
58
114
  @is_resource
59
115
  end
60
116
 
117
+ # @return [Boolean] Inverse of `#resource?`.
61
118
  def collection?
62
119
  !@is_resource
63
120
  end
64
121
 
65
- def parse_query_params(path)
66
- query_params_fast(path)
67
- end
68
-
69
- # Optimized path generation with better caching and fewer allocations
70
- def generate_path(params = {})
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?
76
-
77
- merged = build_merged_params(params)
78
- cache_key = cache_key_for_params(@required_params, merged)
79
- if (cached = @gen_cache.get(cache_key))
80
- return cached
81
- end
82
- path_str = generate_path_string(merged)
83
- @gen_cache.set(cache_key, path_str)
84
- path_str
85
- end
86
-
87
- # Fast query params method (cached and optimized)
88
- def query_params(request_path)
89
- query_params_fast(request_path)
90
- end
91
-
92
122
  private
93
123
 
94
- # Constants for performance
95
- EMPTY_HASH = {}.freeze
96
- ROOT_PATH = '/'.freeze
97
- UNRESERVED_RE = /\A[a-zA-Z0-9\-._~]+\z/.freeze
98
- QUERY_CACHE_SIZE = 128
99
-
100
- # Common HTTP methods - interned for performance
101
- HTTP_GET = 'GET'.freeze
102
- HTTP_POST = 'POST'.freeze
103
- HTTP_PUT = 'PUT'.freeze
104
- HTTP_PATCH = 'PATCH'.freeze
105
- HTTP_DELETE = 'DELETE'.freeze
106
- HTTP_HEAD = 'HEAD'.freeze
107
- HTTP_OPTIONS = 'OPTIONS'.freeze
108
-
109
- # Fast method normalization using interned constants
110
- def normalize_method(method)
111
- case method
112
- when :get then HTTP_GET
113
- when :post then HTTP_POST
114
- when :put then HTTP_PUT
115
- when :patch then HTTP_PATCH
116
- when :delete then HTTP_DELETE
117
- when :head then HTTP_HEAD
118
- when :options then HTTP_OPTIONS
119
- else method.to_s.upcase.freeze
120
- end
121
- end
122
-
123
- # Pre-compile all route data at initialization
124
- def precompile_route_data
125
- @is_resource = @path.match?(/\/:id(?:$|\.)/)
126
- @gen_cache = SmallLru.new(512) # larger cache
127
- @query_cache = SmallLru.new(QUERY_CACHE_SIZE)
128
- initialize_validation_cache
129
-
130
- compile_segments
131
- compile_required_params
132
- check_static_path
133
- end
134
-
135
- def compile_segments
136
- @compiled_segments = if @path == ROOT_PATH
137
- EMPTY_ARRAY
138
- else
139
- @path.split('/').reject(&:empty?).map do |seg|
140
- RubyRoutes::Constant.segment_descriptor(seg)
141
- end.freeze
142
- end
143
- end
144
-
145
- def compile_required_params
146
- param_names = @compiled_segments.filter_map { |s| s[:name] if s[:type] != :static }
147
- @param_names = param_names.freeze
148
- @required_params = param_names.reject { |n| @defaults.key?(n) }.freeze
149
- @required_params_set = @required_params.to_set.freeze
150
- end
151
-
152
- def check_static_path
153
- # Pre-generate static paths (no params)
154
- if @required_params.empty?
155
- @static_path = generate_static_path
156
- end
157
- end
158
-
159
- def generate_static_path
160
- return ROOT_PATH if @compiled_segments.empty?
161
-
162
- parts = @compiled_segments.map { |seg| seg[:value] }
163
- "/#{parts.join('/')}"
164
- end
165
-
166
- # Optimized param building
167
- def build_params_hash(path_params, request_path, parsed_qp)
168
- # Use pre-allocated hash when possible
169
- result = get_thread_local_hash
170
-
171
- # Path params first (highest priority)
172
- result.update(path_params)
173
-
174
- # Query params (if needed)
175
- if parsed_qp
176
- result.merge!(parsed_qp)
177
- elsif request_path.include?('?')
178
- qp = query_params_fast(request_path)
179
- result.merge!(qp) unless qp.empty?
180
- end
181
-
182
- # Defaults (lowest priority)
183
- merge_defaults_fast(result) unless @defaults.empty?
184
-
185
- # Validate constraints efficiently
186
- validate_constraints_fast!(result) unless @constraints.empty?
187
-
188
- result
189
- end
190
-
191
- def get_thread_local_hash
192
- # Use a pool of hashes to reduce allocations
193
- pool = Thread.current[:ruby_routes_hash_pool] ||= []
194
- if pool.empty?
195
- {}
196
- else
197
- hash = pool.pop
198
- hash.clear
199
- hash
200
- end
201
- end
202
-
203
- def return_hash_to_pool(hash)
204
- pool = Thread.current[:ruby_routes_hash_pool] ||= []
205
- pool.push(hash) if pool.size < 5 # Keep pool small to avoid memory bloat
206
- end
207
-
208
- def merge_defaults_fast(result)
209
- @defaults.each { |k, v| result[k] = v unless result.key?(k) }
210
- end
211
-
212
- # Fast path parameter extraction
213
- def extract_path_params_fast(request_path)
214
- return EMPTY_HASH if @compiled_segments.empty? && request_path == ROOT_PATH
215
- return nil if @compiled_segments.empty?
216
-
217
- path_parts = split_path(request_path)
218
-
219
- # Check for wildcard/splat segment
220
- has_splat = @compiled_segments.any? { |seg| seg[:type] == :splat }
221
-
222
- if has_splat
223
- return nil if path_parts.size < @compiled_segments.size - 1
224
- else
225
- return nil if @compiled_segments.size != path_parts.size
226
- end
227
-
228
- extract_params_from_parts(path_parts)
229
- end
230
-
231
- def extract_params_from_parts(path_parts)
232
- params = {}
233
-
234
- @compiled_segments.each_with_index do |seg, idx|
235
- case seg[:type]
236
- when :static
237
- return nil unless seg[:value] == path_parts[idx]
238
- when :param
239
- params[seg[:name]] = path_parts[idx]
240
- when :splat
241
- params[seg[:name]] = path_parts[idx..-1].join('/')
242
- break
243
- end
244
- end
245
-
246
- params
247
- end
248
-
249
- # Optimized merged params building
250
- def build_merged_params(params)
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
259
- end
260
- h
261
- end
262
-
263
- def get_thread_local_merged_hash
264
- hash = Thread.current[:ruby_routes_merged] ||= {}
265
- hash.clear
266
- hash
267
- end
268
-
269
- # Optimized path generation
270
- def generate_path_string(merged)
271
- return ROOT_PATH if @compiled_segments.empty?
272
-
273
- # Estimate final path length to avoid resizing
274
- estimated_size = 1 # For leading slash
275
- @compiled_segments.each do |seg|
276
- case seg[:type]
277
- when :static
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]
294
- when :param
295
- value = merged.fetch(seg[:name]).to_s
296
- path << encode_segment_fast(value)
297
- when :splat
298
- value = merged.fetch(seg[:name], '')
299
- path << format_splat_value(value)
300
- end
301
-
302
- path << '/' unless i == last_idx
303
- end
304
-
305
- path
306
- end
307
-
308
- def format_splat_value(value)
309
- case value
310
- when Array
311
- value.map { |part| encode_segment_fast(part.to_s) }.join('/')
312
- when String
313
- value.split('/').map { |part| encode_segment_fast(part) }.join('/')
314
- else
315
- encode_segment_fast(value.to_s)
316
- end
317
- end
318
-
319
- # Fast segment encoding with caching for common values
320
- def encode_segment_fast(str)
321
- return str if UNRESERVED_RE.match?(str)
322
-
323
- # Cache encoded segments to avoid repeated encoding
324
- @encoding_cache ||= {}
325
- @encoding_cache[str] ||= begin
326
- # Use URI.encode_www_form_component but replace + with %20 for path segments
327
- URI.encode_www_form_component(str).gsub('+', '%20')
328
- end
329
- end
330
-
331
- # Optimized query params with caching
332
- def query_params_fast(path)
333
- query_start = path.index('?')
334
- return EMPTY_HASH unless query_start
335
-
336
- query_string = path[(query_start + 1)..-1]
337
- return EMPTY_HASH if query_string.empty? || query_string.match?(/^\?+$/)
338
-
339
- # Cache query param parsing
340
- if (cached = @query_cache.get(query_string))
341
- return cached
342
- end
343
-
344
- result = Rack::Utils.parse_query(query_string)
345
- @query_cache.set(query_string, result)
346
- result
347
- end
348
-
124
+ # Set up HTTP methods from options.
125
+ #
126
+ # @param options [Hash] The options for the route.
127
+ # @return [void]
128
+ def setup_methods(options)
129
+ raw_http_methods = Array(options[:via] || :get)
130
+ @methods = raw_http_methods.map { |method| normalize_http_method(method) }.freeze
131
+ @methods_set = @methods.to_set.freeze
132
+ end
133
+
134
+ # Set up controller and action from options.
135
+ #
136
+ # @param options [Hash] The options for the route.
137
+ # @return [void]
138
+ def setup_controller_and_action(options)
139
+ @controller = extract_controller(options)
140
+ @action = options[:action] || extract_action(options[:to])
141
+ end
142
+
143
+ # Infer controller name from options or `:to`.
144
+ #
145
+ # @param options [Hash] The options for the route.
146
+ # @return [String, nil] The inferred controller name.
349
147
  def extract_controller(options)
350
148
  to = options[:to]
351
149
  return options[:controller] unless to
150
+
352
151
  to.to_s.split('#', 2).first
353
152
  end
354
153
 
154
+ # Infer action from `:to` string.
155
+ #
156
+ # @param to [String, nil] The `:to` string.
157
+ # @return [String, nil] The inferred action name.
355
158
  def extract_action(to)
356
159
  return nil unless to
357
- to.to_s.split('#', 2).last
358
- end
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
160
 
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
-
438
- # Optimized constraint validation
439
- def validate_constraints_fast!(params)
440
- @constraints.each do |param, constraint|
441
- value = params[param.to_s]
442
- # Only skip validation if the parameter is completely missing from params
443
- # Empty strings and nil values should still be validated
444
- next unless params.key?(param.to_s)
445
-
446
- case constraint
447
- when Regexp
448
- # Protect against ReDoS attacks with timeout
449
- begin
450
- Timeout.timeout(0.1) do
451
- raise RubyRoutes::ConstraintViolation unless constraint.match?(value.to_s)
452
- end
453
- rescue Timeout::Error
454
- raise RubyRoutes::ConstraintViolation, "Regex constraint timed out (potential ReDoS attack)"
455
- end
456
- when Proc
457
- # DEPRECATED: Proc constraints are deprecated due to security risks
458
- warn_proc_constraint_deprecation(param)
459
-
460
- # For backward compatibility, still execute but with strict timeout
461
- begin
462
- Timeout.timeout(0.05) do # Reduced timeout for security
463
- raise RubyRoutes::ConstraintViolation unless constraint.call(value.to_s)
464
- end
465
- rescue Timeout::Error
466
- raise RubyRoutes::ConstraintViolation, "Proc constraint timed out (consider using secure alternatives)"
467
- rescue => e
468
- raise RubyRoutes::ConstraintViolation, "Proc constraint failed: #{e.message}"
469
- end
470
- when :int
471
- value_str = value.to_s
472
- raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A\d+\z/)
473
- when :uuid
474
- value_str = value.to_s
475
- raise RubyRoutes::ConstraintViolation unless value_str.length == 36 &&
476
- value_str.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
477
- when :email
478
- value_str = value.to_s
479
- raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
480
- when :slug
481
- value_str = value.to_s
482
- raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[a-z0-9]+(?:-[a-z0-9]+)*\z/)
483
- when :alpha
484
- value_str = value.to_s
485
- raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[a-zA-Z]+\z/)
486
- when :alphanumeric
487
- value_str = value.to_s
488
- raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[a-zA-Z0-9]+\z/)
489
- when Hash
490
- # Secure hash-based constraints for common patterns
491
- validate_hash_constraint!(constraint, value_str = value.to_s)
492
- end
493
- end
494
- end
495
-
496
- def warn_proc_constraint_deprecation(param)
497
- return if @proc_warnings_shown&.include?(param)
498
-
499
- @proc_warnings_shown ||= Set.new
500
- @proc_warnings_shown << param
501
-
502
- warn <<~WARNING
503
- [DEPRECATION] Proc constraints are deprecated due to security risks.
504
-
505
- Parameter: #{param}
506
- Route: #{@path}
507
-
508
- Secure alternatives:
509
- - Use regex: constraints: { #{param}: /\\A\\d+\\z/ }
510
- - Use built-in types: constraints: { #{param}: :int }
511
- - Use hash constraints: constraints: { #{param}: { min_length: 3, format: /\\A[a-z]+\\z/ } }
512
-
513
- Available built-in types: :int, :uuid, :email, :slug, :alpha, :alphanumeric
514
-
515
- This warning will become an error in a future version.
516
- WARNING
517
- end
518
-
519
- def validate_hash_constraint!(constraint, value)
520
- # Secure hash-based constraints
521
- if constraint[:min_length] && value.length < constraint[:min_length]
522
- raise RubyRoutes::ConstraintViolation, "Value too short (minimum #{constraint[:min_length]} characters)"
523
- end
524
-
525
- if constraint[:max_length] && value.length > constraint[:max_length]
526
- raise RubyRoutes::ConstraintViolation, "Value too long (maximum #{constraint[:max_length]} characters)"
527
- end
528
-
529
- if constraint[:format] && !value.match?(constraint[:format])
530
- raise RubyRoutes::ConstraintViolation, "Value does not match required format"
531
- end
532
-
533
- if constraint[:in] && !constraint[:in].include?(value)
534
- raise RubyRoutes::ConstraintViolation, "Value not in allowed list"
535
- end
536
-
537
- if constraint[:not_in] && constraint[:not_in].include?(value)
538
- raise RubyRoutes::ConstraintViolation, "Value in forbidden list"
539
- end
540
-
541
- if constraint[:range] && !constraint[:range].cover?(value.to_i)
542
- raise RubyRoutes::ConstraintViolation, "Value not in allowed range"
543
- end
161
+ to.to_s.split('#', 2).last
544
162
  end
545
163
 
546
- def validate_route!
547
- raise InvalidRoute, "Controller is required" if @controller.nil?
548
- raise InvalidRoute, "Action is required" if @action.nil?
549
- raise InvalidRoute, "Invalid HTTP method: #{@methods}" if @methods.empty?
164
+ # Precompile route data for performance.
165
+ #
166
+ # @return [void]
167
+ def precompile_route_data
168
+ @is_resource = @path.match?(%r{/:id(?:$|\.)})
169
+ @gen_cache = SmallLru.new(512)
170
+ @query_cache = SmallLru.new(RubyRoutes::Constant::QUERY_CACHE_SIZE)
171
+ @cache_mutex = Mutex.new # Thread-safe access to caches
172
+ initialize_validation_cache
173
+ compile_segments
174
+ compile_required_params
175
+ check_static_path
550
176
  end
551
177
  end
552
- end
178
+ end