ruby_routes 2.1.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 +82 -41
  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 +83 -142
  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 +96 -17
  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 +121 -451
  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 +126 -148
  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 +196 -179
  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 +179 -0
  39. data/lib/ruby_routes/utility/method_utility.rb +137 -0
  40. data/lib/ruby_routes/utility/path_utility.rb +89 -0
  41. data/lib/ruby_routes/utility/route_utility.rb +49 -0
  42. data/lib/ruby_routes/version.rb +3 -1
  43. data/lib/ruby_routes.rb +68 -11
  44. metadata +30 -7
@@ -1,505 +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'
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'
6
21
 
7
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
8
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
59
+ include RubyRoutes::Utility::PathUtility
60
+ include RubyRoutes::Utility::KeyBuilderUtility
61
+
9
62
  attr_reader :path, :methods, :controller, :action, :name, :constraints, :defaults
10
63
 
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.
11
77
  def initialize(path, options = {})
12
78
  @path = normalize_path(path)
13
- # 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])
79
+
80
+ setup_methods(options)
81
+ setup_controller_and_action(options)
82
+
19
83
  @name = options[:as]
20
84
  @constraints = options[:constraints] || {}
21
- # Pre-normalize defaults to string keys and freeze
22
85
  @defaults = (options[:defaults] || {}).transform_keys(&:to_s).freeze
86
+ @param_key_slots = [[nil, nil], [nil, nil]]
87
+ @required_validated_once = false
23
88
 
24
- # Pre-compile everything at initialization
25
89
  precompile_route_data
26
90
  validate_route!
27
91
  end
28
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.
29
98
  def match?(request_method, request_path)
30
- # Fast method check: use frozen Set for O(1) lookup
31
- return false unless @methods_set.include?(request_method.to_s.upcase)
32
- !!extract_path_params_fast(request_path)
33
- end
34
-
35
- def extract_params(request_path, parsed_qp = nil)
36
- path_params = extract_path_params_fast(request_path)
99
+ normalized_method = normalize_http_method(request_method)
100
+ return false unless @methods_set.include?(normalized_method)
37
101
 
38
- return EMPTY_HASH unless path_params
39
-
40
- # Use optimized param building
41
- build_params_hash(path_params, request_path, parsed_qp)
102
+ !!extract_path_params_fast(request_path)
42
103
  end
43
104
 
105
+ # @return [Boolean] Whether this route has a name.
44
106
  def named?
45
107
  !@name.nil?
46
108
  end
47
109
 
110
+ # @return [Boolean] Heuristic: path contains `:id` implying a resource member.
48
111
  def resource?
49
112
  @is_resource
50
113
  end
51
114
 
115
+ # @return [Boolean] Inverse of `#resource?`.
52
116
  def collection?
53
117
  !@is_resource
54
118
  end
55
119
 
56
- def parse_query_params(path)
57
- query_params_fast(path)
58
- end
59
-
60
- # Optimized path generation with better caching and fewer allocations
61
- def generate_path(params = {})
62
- return ROOT_PATH if @path == ROOT_PATH
63
-
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
- 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)
86
- if (cached = @gen_cache.get(cache_key))
87
- return cached
88
- end
89
-
90
- # Generate path using string buffer (avoid array allocations)
91
- path_str = generate_path_string(merged)
92
- @gen_cache.set(cache_key, path_str)
93
- path_str
94
- end
95
-
96
- # Fast query params method (cached and optimized)
97
- def query_params(request_path)
98
- query_params_fast(request_path)
99
- end
100
-
101
120
  private
102
121
 
103
- # Constants for performance
104
- EMPTY_HASH = {}.freeze
105
- ROOT_PATH = '/'.freeze
106
- UNRESERVED_RE = /\A[a-zA-Z0-9\-._~]+\z/.freeze
107
- QUERY_CACHE_SIZE = 128
108
-
109
- # Common HTTP methods - interned for performance
110
- HTTP_GET = 'GET'.freeze
111
- HTTP_POST = 'POST'.freeze
112
- HTTP_PUT = 'PUT'.freeze
113
- HTTP_PATCH = 'PATCH'.freeze
114
- HTTP_DELETE = 'DELETE'.freeze
115
- HTTP_HEAD = 'HEAD'.freeze
116
- HTTP_OPTIONS = 'OPTIONS'.freeze
117
-
118
- # Fast method normalization using interned constants
119
- def normalize_method(method)
120
- case method
121
- when :get then HTTP_GET
122
- when :post then HTTP_POST
123
- when :put then HTTP_PUT
124
- when :patch then HTTP_PATCH
125
- when :delete then HTTP_DELETE
126
- when :head then HTTP_HEAD
127
- when :options then HTTP_OPTIONS
128
- else method.to_s.upcase.freeze
129
- end
130
- end
131
-
132
- # Pre-compile all route data at initialization
133
- def precompile_route_data
134
- @is_resource = @path.match?(/\/:id(?:$|\.)/)
135
- @gen_cache = SmallLru.new(512) # larger cache
136
- @query_cache = SmallLru.new(QUERY_CACHE_SIZE)
137
-
138
- compile_segments
139
- compile_required_params
140
- check_static_path
141
- end
142
-
143
- def compile_segments
144
- @compiled_segments = if @path == ROOT_PATH
145
- EMPTY_ARRAY
146
- else
147
- @path.split('/').reject(&:empty?).map do |seg|
148
- RubyRoutes::Constant.segment_descriptor(seg)
149
- end.freeze
150
- end
151
- end
152
-
153
- def compile_required_params
154
- param_names = @compiled_segments.filter_map { |s| s[:name] if s[:type] != :static }
155
- @param_names = param_names.freeze
156
- @required_params = param_names.reject { |n| @defaults.key?(n) }.freeze
157
- @required_params_set = @required_params.to_set.freeze
158
- end
159
-
160
- def check_static_path
161
- # Pre-generate static paths (no params)
162
- if @required_params.empty?
163
- @static_path = generate_static_path
164
- end
165
- end
166
-
167
- def generate_static_path
168
- return ROOT_PATH if @compiled_segments.empty?
169
-
170
- parts = @compiled_segments.map { |seg| seg[:value] }
171
- "/#{parts.join('/')}"
172
- end
173
-
174
- # Optimized param building
175
- def build_params_hash(path_params, request_path, parsed_qp)
176
- # Use pre-allocated hash when possible
177
- result = get_thread_local_hash
178
-
179
- # Path params first (highest priority)
180
- result.update(path_params)
181
-
182
- # Query params (if needed)
183
- if parsed_qp
184
- result.merge!(parsed_qp)
185
- elsif request_path.include?('?')
186
- qp = query_params_fast(request_path)
187
- result.merge!(qp) unless qp.empty?
188
- end
189
-
190
- # Defaults (lowest priority)
191
- merge_defaults_fast(result) unless @defaults.empty?
192
-
193
- # Validate constraints efficiently
194
- validate_constraints_fast!(result) unless @constraints.empty?
195
-
196
- result.dup
197
- end
198
-
199
- def get_thread_local_hash
200
- # Use a pool of hashes to reduce allocations
201
- pool = Thread.current[:ruby_routes_hash_pool] ||= []
202
- if pool.empty?
203
- {}
204
- else
205
- hash = pool.pop
206
- hash.clear
207
- hash
208
- end
209
- end
210
-
211
- def return_hash_to_pool(hash)
212
- pool = Thread.current[:ruby_routes_hash_pool] ||= []
213
- pool.push(hash) if pool.size < 5 # Keep pool small to avoid memory bloat
214
- end
215
-
216
- def merge_defaults_fast(result)
217
- @defaults.each { |k, v| result[k] = v unless result.key?(k) }
218
- end
219
-
220
- # Fast path parameter extraction
221
- def extract_path_params_fast(request_path)
222
- return EMPTY_HASH if @compiled_segments.empty? && request_path == ROOT_PATH
223
- return nil if @compiled_segments.empty?
224
-
225
- path_parts = split_path_fast(request_path)
226
-
227
- # Check for wildcard/splat segment
228
- has_splat = @compiled_segments.any? { |seg| seg[:type] == :splat }
229
-
230
- if has_splat
231
- return nil if path_parts.size < @compiled_segments.size - 1
232
- else
233
- return nil if @compiled_segments.size != path_parts.size
234
- end
235
-
236
- extract_params_from_parts(path_parts)
237
- end
238
-
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
- def extract_params_from_parts(path_parts)
248
- params = {}
249
-
250
- @compiled_segments.each_with_index do |seg, idx|
251
- case seg[:type]
252
- when :static
253
- return nil unless seg[:value] == path_parts[idx]
254
- when :param
255
- params[seg[:name]] = path_parts[idx]
256
- when :splat
257
- params[seg[:name]] = path_parts[idx..-1].join('/')
258
- break
259
- end
260
- end
261
-
262
- params
263
- end
264
-
265
- # Optimized merged params building
266
- 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 }
280
- end
281
-
282
- merged
283
- end
284
-
285
- def get_thread_local_merged_hash
286
- hash = Thread.current[:ruby_routes_merged] ||= {}
287
- hash.clear
288
- hash
289
- end
290
-
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
- # Optimized path generation
304
- def generate_path_string(merged)
305
- return ROOT_PATH if @compiled_segments.empty?
306
-
307
- # Pre-allocate array for parts to avoid string buffer operations
308
- parts = []
309
-
310
- @compiled_segments.each do |seg|
311
- case seg[:type]
312
- when :static
313
- parts << seg[:value]
314
- when :param
315
- value = merged.fetch(seg[:name]).to_s
316
- parts << encode_segment_fast(value)
317
- when :splat
318
- value = merged.fetch(seg[:name], '')
319
- parts << format_splat_value(value)
320
- end
321
- end
322
-
323
- # Single join operation is faster than multiple string concatenations
324
- path = "/#{parts.join('/')}"
325
- path == '/' ? ROOT_PATH : path
326
- end
327
-
328
- def format_splat_value(value)
329
- case value
330
- when Array
331
- value.map { |part| encode_segment_fast(part.to_s) }.join('/')
332
- when String
333
- value.split('/').map { |part| encode_segment_fast(part) }.join('/')
334
- else
335
- encode_segment_fast(value.to_s)
336
- end
337
- end
338
-
339
- # Fast segment encoding with caching for common values
340
- def encode_segment_fast(str)
341
- return str if UNRESERVED_RE.match?(str)
342
-
343
- # Cache encoded segments to avoid repeated encoding
344
- @encoding_cache ||= {}
345
- @encoding_cache[str] ||= begin
346
- # Use URI.encode_www_form_component but replace + with %20 for path segments
347
- URI.encode_www_form_component(str).gsub('+', '%20')
348
- end
349
- end
350
-
351
- # Optimized query params with caching
352
- def query_params_fast(path)
353
- query_start = path.index('?')
354
- return EMPTY_HASH unless query_start
355
-
356
- query_string = path[(query_start + 1)..-1]
357
- return EMPTY_HASH if query_string.empty? || query_string.match?(/^\?+$/)
358
-
359
- # Cache query param parsing
360
- if (cached = @query_cache.get(query_string))
361
- return cached
362
- end
363
-
364
- result = Rack::Utils.parse_query(query_string)
365
- @query_cache.set(query_string, result)
366
- result
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
367
130
  end
368
131
 
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
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])
374
139
  end
375
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.
376
145
  def extract_controller(options)
377
146
  to = options[:to]
378
147
  return options[:controller] unless to
148
+
379
149
  to.to_s.split('#', 2).first
380
150
  end
381
151
 
152
+ # Infer action from `:to` string.
153
+ #
154
+ # @param to [String, nil] The `:to` string.
155
+ # @return [String, nil] The inferred action name.
382
156
  def extract_action(to)
383
157
  return nil unless to
384
- to.to_s.split('#', 2).last
385
- end
386
-
387
- # Optimized constraint validation
388
- def validate_constraints_fast!(params)
389
- @constraints.each do |param, constraint|
390
- value = params[param.to_s]
391
- # Only skip validation if the parameter is completely missing from params
392
- # Empty strings and nil values should still be validated
393
- next unless params.key?(param.to_s)
394
-
395
- case constraint
396
- when Regexp
397
- # Protect against ReDoS attacks with timeout
398
- begin
399
- Timeout.timeout(0.1) do
400
- raise RubyRoutes::ConstraintViolation unless constraint.match?(value.to_s)
401
- end
402
- rescue Timeout::Error
403
- raise RubyRoutes::ConstraintViolation, "Regex constraint timed out (potential ReDoS attack)"
404
- end
405
- when Proc
406
- # DEPRECATED: Proc constraints are deprecated due to security risks
407
- warn_proc_constraint_deprecation(param)
408
-
409
- # For backward compatibility, still execute but with strict timeout
410
- begin
411
- Timeout.timeout(0.05) do # Reduced timeout for security
412
- raise RubyRoutes::ConstraintViolation unless constraint.call(value.to_s)
413
- end
414
- rescue Timeout::Error
415
- raise RubyRoutes::ConstraintViolation, "Proc constraint timed out (consider using secure alternatives)"
416
- rescue => e
417
- raise RubyRoutes::ConstraintViolation, "Proc constraint failed: #{e.message}"
418
- end
419
- when :int
420
- value_str = value.to_s
421
- raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A\d+\z/)
422
- when :uuid
423
- value_str = value.to_s
424
- raise RubyRoutes::ConstraintViolation unless value_str.length == 36 &&
425
- 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)
426
- when :email
427
- value_str = value.to_s
428
- raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
429
- when :slug
430
- value_str = value.to_s
431
- raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[a-z0-9]+(?:-[a-z0-9]+)*\z/)
432
- when :alpha
433
- value_str = value.to_s
434
- raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[a-zA-Z]+\z/)
435
- when :alphanumeric
436
- value_str = value.to_s
437
- raise RubyRoutes::ConstraintViolation unless value_str.match?(/\A[a-zA-Z0-9]+\z/)
438
- when Hash
439
- # Secure hash-based constraints for common patterns
440
- validate_hash_constraint!(constraint, value_str = value.to_s)
441
- end
442
- end
443
- end
444
-
445
- def warn_proc_constraint_deprecation(param)
446
- return if @proc_warnings_shown&.include?(param)
447
-
448
- @proc_warnings_shown ||= Set.new
449
- @proc_warnings_shown << param
450
-
451
- warn <<~WARNING
452
- [DEPRECATION] Proc constraints are deprecated due to security risks.
453
-
454
- Parameter: #{param}
455
- Route: #{@path}
456
158
 
457
- Secure alternatives:
458
- - Use regex: constraints: { #{param}: /\\A\\d+\\z/ }
459
- - Use built-in types: constraints: { #{param}: :int }
460
- - Use hash constraints: constraints: { #{param}: { min_length: 3, format: /\\A[a-z]+\\z/ } }
461
-
462
- Available built-in types: :int, :uuid, :email, :slug, :alpha, :alphanumeric
463
-
464
- This warning will become an error in a future version.
465
- WARNING
466
- end
467
-
468
- def validate_hash_constraint!(constraint, value)
469
- # Secure hash-based constraints
470
- if constraint[:min_length] && value.length < constraint[:min_length]
471
- raise RubyRoutes::ConstraintViolation, "Value too short (minimum #{constraint[:min_length]} characters)"
472
- end
473
-
474
- if constraint[:max_length] && value.length > constraint[:max_length]
475
- raise RubyRoutes::ConstraintViolation, "Value too long (maximum #{constraint[:max_length]} characters)"
476
- end
477
-
478
- if constraint[:format] && !value.match?(constraint[:format])
479
- raise RubyRoutes::ConstraintViolation, "Value does not match required format"
480
- end
481
-
482
- if constraint[:in] && !constraint[:in].include?(value)
483
- raise RubyRoutes::ConstraintViolation, "Value not in allowed list"
484
- end
485
-
486
- if constraint[:not_in] && constraint[:not_in].include?(value)
487
- raise RubyRoutes::ConstraintViolation, "Value in forbidden list"
488
- end
489
-
490
- if constraint[:range] && !constraint[:range].cover?(value.to_i)
491
- raise RubyRoutes::ConstraintViolation, "Value not in allowed range"
492
- end
159
+ to.to_s.split('#', 2).last
493
160
  end
494
161
 
495
- def validate_route!
496
- raise InvalidRoute, "Controller is required" if @controller.nil?
497
- raise InvalidRoute, "Action is required" if @action.nil?
498
- 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
499
173
  end
500
-
501
- # Additional constants
502
- EMPTY_ARRAY = [].freeze
503
- EMPTY_STRING = ''.freeze
504
174
  end
505
- end
175
+ end