ruby_routes 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +232 -162
  3. data/lib/ruby_routes/constant.rb +137 -18
  4. data/lib/ruby_routes/lru_strategies/hit_strategy.rb +31 -4
  5. data/lib/ruby_routes/lru_strategies/miss_strategy.rb +21 -0
  6. data/lib/ruby_routes/node.rb +75 -33
  7. data/lib/ruby_routes/radix_tree/finder.rb +164 -0
  8. data/lib/ruby_routes/radix_tree/inserter.rb +98 -0
  9. data/lib/ruby_routes/radix_tree.rb +79 -227
  10. data/lib/ruby_routes/route/check_helpers.rb +109 -0
  11. data/lib/ruby_routes/route/constraint_validator.rb +159 -0
  12. data/lib/ruby_routes/route/param_support.rb +202 -0
  13. data/lib/ruby_routes/route/path_builder.rb +86 -0
  14. data/lib/ruby_routes/route/path_generation.rb +102 -0
  15. data/lib/ruby_routes/route/query_helpers.rb +56 -0
  16. data/lib/ruby_routes/route/segment_compiler.rb +163 -0
  17. data/lib/ruby_routes/route/small_lru.rb +93 -18
  18. data/lib/ruby_routes/route/validation_helpers.rb +151 -0
  19. data/lib/ruby_routes/route/warning_helpers.rb +54 -0
  20. data/lib/ruby_routes/route.rb +124 -501
  21. data/lib/ruby_routes/route_set/cache_helpers.rb +174 -0
  22. data/lib/ruby_routes/route_set/collection_helpers.rb +127 -0
  23. data/lib/ruby_routes/route_set.rb +120 -133
  24. data/lib/ruby_routes/router/build_helpers.rb +100 -0
  25. data/lib/ruby_routes/router/builder.rb +96 -0
  26. data/lib/ruby_routes/router/http_helpers.rb +135 -0
  27. data/lib/ruby_routes/router/resource_helpers.rb +137 -0
  28. data/lib/ruby_routes/router/scope_helpers.rb +109 -0
  29. data/lib/ruby_routes/router.rb +193 -181
  30. data/lib/ruby_routes/segment.rb +28 -8
  31. data/lib/ruby_routes/segments/base_segment.rb +40 -4
  32. data/lib/ruby_routes/segments/dynamic_segment.rb +48 -12
  33. data/lib/ruby_routes/segments/static_segment.rb +43 -7
  34. data/lib/ruby_routes/segments/wildcard_segment.rb +56 -12
  35. data/lib/ruby_routes/string_extensions.rb +52 -15
  36. data/lib/ruby_routes/url_helpers.rb +106 -24
  37. data/lib/ruby_routes/utility/inflector_utility.rb +35 -0
  38. data/lib/ruby_routes/utility/key_builder_utility.rb +161 -84
  39. data/lib/ruby_routes/utility/method_utility.rb +137 -0
  40. data/lib/ruby_routes/utility/path_utility.rb +75 -28
  41. data/lib/ruby_routes/utility/route_utility.rb +30 -2
  42. data/lib/ruby_routes/version.rb +3 -1
  43. data/lib/ruby_routes.rb +68 -11
  44. metadata +27 -7
@@ -1,552 +1,175 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
  require 'timeout'
3
- require 'set'
4
5
  require 'rack'
6
+ require_relative 'constant'
7
+ require_relative 'node'
5
8
  require_relative 'route/small_lru'
6
- require_relative 'utility/path_utility'
7
9
  require_relative 'utility/key_builder_utility'
10
+ require_relative 'utility/method_utility'
11
+ require_relative 'utility/path_utility'
12
+ require_relative 'utility/route_utility'
13
+ require_relative 'route/param_support'
14
+ require_relative 'route/segment_compiler'
15
+ require_relative 'route/path_builder'
16
+ require_relative 'route/constraint_validator'
17
+ require_relative 'route/check_helpers'
18
+ require_relative 'route/query_helpers'
19
+ require_relative 'route/validation_helpers'
20
+ require_relative 'route/path_generation'
8
21
 
9
22
  module RubyRoutes
23
+ # Route
24
+ #
25
+ # Immutable-ish representation of a single HTTP route plus optimized
26
+ # helpers for:
27
+ # - Path recognition (segment compilation + fast param extraction)
28
+ # - Path generation (low‑allocation caching + param merging)
29
+ # - Constraint validation (regexp / typed / hash rules)
30
+ #
31
+ # Performance Techniques:
32
+ # - Precompiled segment descriptors (static / param / splat)
33
+ # - Small LRU caches (path generation + query parsing)
34
+ # - Thread‑local reusable Hashes / String buffers
35
+ # - Minimal object allocation in hot paths
36
+ #
37
+ # Thread Safety:
38
+ # - Instance is effectively read‑only after initialization aside from
39
+ # internal caches which are not synchronized but safe for typical
40
+ # single writer (boot) + many reader (request) usage.
41
+ #
42
+ # Public API Surface (stable):
43
+ # - #match?
44
+ # - #extract_params
45
+ # - #generate_path
46
+ # - #named?
47
+ # - #resource? / #collection?
48
+ #
49
+ # @api public
10
50
  class Route
51
+ include ParamSupport
52
+ include SegmentCompiler
53
+ include PathBuilder
54
+ include RubyRoutes::Route::ConstraintValidator
55
+ include RubyRoutes::Route::ValidationHelpers
56
+ include RubyRoutes::Route::QueryHelpers
57
+ include RubyRoutes::Route::PathGeneration
58
+ include RubyRoutes::Utility::MethodUtility
11
59
  include RubyRoutes::Utility::PathUtility
12
60
  include RubyRoutes::Utility::KeyBuilderUtility
13
61
 
14
62
  attr_reader :path, :methods, :controller, :action, :name, :constraints, :defaults
15
63
 
16
- EMPTY_ARRAY = [].freeze
17
- EMPTY_PAIR = [EMPTY_ARRAY, EMPTY_ARRAY].freeze
18
- EMPTY_STRING = ''.freeze
19
-
64
+ public :extract_params, :parse_query_params, :query_params, :generate_path
65
+
66
+ # Create a new Route.
67
+ #
68
+ # @param path [String] The raw route path (may include `:params` or `*splat`).
69
+ # @param options [Hash] The options for the route.
70
+ # @option options [Symbol, String, Array<Symbol, String>] :via (:get) HTTP method(s).
71
+ # @option options [String] :to ("controller#action") The controller and action.
72
+ # @option options [String] :controller Explicit controller (overrides `:to`).
73
+ # @option options [String, Symbol] :action Explicit action (overrides part after `#`).
74
+ # @option options [Hash] :constraints Parameter constraints (Regexp / Symbol / Hash).
75
+ # @option options [Hash] :defaults Default parameter values.
76
+ # @option options [Symbol, String] :as The route name.
20
77
  def initialize(path, options = {})
21
78
  @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
79
 
33
- # Pre-compile everything at initialization
80
+ setup_methods(options)
81
+ setup_controller_and_action(options)
82
+
83
+ @name = options[:as]
84
+ @constraints = options[:constraints] || {}
85
+ @defaults = (options[:defaults] || {}).transform_keys(&:to_s).freeze
86
+ @param_key_slots = [[nil, nil], [nil, nil]]
87
+ @required_validated_once = false
88
+
34
89
  precompile_route_data
35
90
  validate_route!
36
91
  end
37
92
 
93
+ # Test if this route matches an HTTP method + path string.
94
+ #
95
+ # @param request_method [String, Symbol] The HTTP method.
96
+ # @param request_path [String] The request path.
97
+ # @return [Boolean] `true` if the route matches, `false` otherwise.
38
98
  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
99
+ normalized_method = normalize_http_method(request_method)
100
+ return false unless @methods_set.include?(normalized_method)
43
101
 
44
- def extract_params(request_path, parsed_qp = nil)
45
- path_params = extract_path_params_fast(request_path)
46
-
47
- return EMPTY_HASH unless path_params
48
-
49
- # Use optimized param building
50
- build_params_hash(path_params, request_path, parsed_qp)
102
+ !!extract_path_params_fast(request_path)
51
103
  end
52
104
 
105
+ # @return [Boolean] Whether this route has a name.
53
106
  def named?
54
107
  !@name.nil?
55
108
  end
56
109
 
110
+ # @return [Boolean] Heuristic: path contains `:id` implying a resource member.
57
111
  def resource?
58
112
  @is_resource
59
113
  end
60
114
 
115
+ # @return [Boolean] Inverse of `#resource?`.
61
116
  def collection?
62
117
  !@is_resource
63
118
  end
64
119
 
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
120
  private
93
121
 
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
-
122
+ # Set up HTTP methods from options.
123
+ #
124
+ # @param options [Hash] The options for the route.
125
+ # @return [void]
126
+ def setup_methods(options)
127
+ raw_http_methods = Array(options[:via] || :get)
128
+ @methods = raw_http_methods.map { |method| normalize_http_method(method) }.freeze
129
+ @methods_set = @methods.to_set.freeze
130
+ end
131
+
132
+ # Set up controller and action from options.
133
+ #
134
+ # @param options [Hash] The options for the route.
135
+ # @return [void]
136
+ def setup_controller_and_action(options)
137
+ @controller = extract_controller(options)
138
+ @action = options[:action] || extract_action(options[:to])
139
+ end
140
+
141
+ # Infer controller name from options or `:to`.
142
+ #
143
+ # @param options [Hash] The options for the route.
144
+ # @return [String, nil] The inferred controller name.
349
145
  def extract_controller(options)
350
146
  to = options[:to]
351
147
  return options[:controller] unless to
148
+
352
149
  to.to_s.split('#', 2).first
353
150
  end
354
151
 
152
+ # Infer action from `:to` string.
153
+ #
154
+ # @param to [String, nil] The `:to` string.
155
+ # @return [String, nil] The inferred action name.
355
156
  def extract_action(to)
356
157
  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
-
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
158
 
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
159
+ to.to_s.split('#', 2).last
544
160
  end
545
161
 
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?
162
+ # Precompile route data for performance.
163
+ #
164
+ # @return [void]
165
+ def precompile_route_data
166
+ @is_resource = @path.match?(%r{/:id(?:$|\.)})
167
+ @gen_cache = SmallLru.new(512)
168
+ @query_cache = SmallLru.new(RubyRoutes::Constant::QUERY_CACHE_SIZE)
169
+ initialize_validation_cache
170
+ compile_segments
171
+ compile_required_params
172
+ check_static_path
550
173
  end
551
174
  end
552
- end
175
+ end