ruby_routes 2.3.0 → 2.5.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: 59f7b87f3f7b94ce54fcc9a8a3d917833c93312412b160cf82268552fd0454ba
4
- data.tar.gz: 40417f1672ac4e849f6a85a9170fd01f1619ef931b8bd58e1f52b8896d23605c
3
+ metadata.gz: 07fa3c9d3d13ac8b73c6f4775871cd5ad6c3cd7c5d0e571797b83fb0dea72189
4
+ data.tar.gz: 2684722163772fe4489d4e8c32eaff036002c5b58e05be68e25e9a0c59cd680b
5
5
  SHA512:
6
- metadata.gz: 7c099d775cf22a7327e8f8b1cfdcb1d51495c08717f20eb879b89e6dbfe2e4ae56cb028ae494e13a72c311c668454d6a6ab8f4e4c7a26a76d0d02ec99ca3c342
7
- data.tar.gz: 754386d02e0387500ae507f5a2e0ad7c11b46a04c7a461aafbee34fd7ad1d19ef7d7ff1ad207375e2e3be205d8af4afa11c06be8d4e2dbedcb8f4e571055994c
6
+ metadata.gz: dfc84b5cfa67c1da89ab8067ad01c8706c81717611969c287a4a536055fe1ee1b4ac80b791248c05dddb090548f1e29006889d2d7f5e91b9058349b9f9a8b317
7
+ data.tar.gz: b2ab1c86ca6838a4164621987c813b7320e3f042d1666576e00c3acf48576204c837fd13582ed42f2cf896334b9e1683c8ceab8868c03b7f9a482dcb808a5052
data/README.md CHANGED
@@ -4,7 +4,7 @@ A high-performance, lightweight routing system for Ruby applications providing a
4
4
 
5
5
  ## Features
6
6
 
7
- - **🚀 High Performance**: 40x faster routing with 99.99% cache hit rate
7
+ - **🚀 High Performance**: Fast routing with 99.99% cache hit rate
8
8
  - **🔄 Rails-like DSL**: Familiar syntax for defining routes
9
9
  - **🛣️ RESTful Resources**: Automatic generation of RESTful routes
10
10
  - **🔒 Secure Constraints**: Built-in security with comprehensive constraint system
@@ -36,7 +36,7 @@ A high-performance, lightweight routing system for Ruby applications providing a
36
36
  Add this line to your application's Gemfile:
37
37
 
38
38
  ```ruby
39
- gem 'ruby_routes', '~> 2.1.0'
39
+ gem 'ruby_routes', '~> 2.3.0'
40
40
  ```
41
41
 
42
42
  And then execute:
@@ -358,6 +358,7 @@ Rack::Handler::WEBrick.run RubyRoutesApp.new, Port: 9292
358
358
  - `scope(options = {})` - Group routes with shared options
359
359
  - `concern(name, &block)` - Define a reusable route concern
360
360
  - `concerns(names)` - Use defined concerns in the current context
361
+ - `build(&block)` - Create a thread-safe, finalized router by accumulating routes in a builder
361
362
 
362
363
  ### RouteSet Methods
363
364
 
@@ -370,7 +371,7 @@ Rack::Handler::WEBrick.run RubyRoutesApp.new, Port: 9292
370
371
 
371
372
  Ruby Routes is optimized for high-performance applications:
372
373
 
373
- - **40x faster** routing compared to many alternatives
374
+ - **Fast** routing
374
375
  - **99.99% cache hit rate** for common access patterns
375
376
  - **Low memory footprint** with bounded caches and object reuse
376
377
  - **Zero memory leaks** in long-running applications
@@ -388,30 +389,36 @@ Performance metrics (from `benchmark/` directory):
388
389
  Ruby Routes prioritizes security with these protections:
389
390
 
390
391
  ### 🔒 Security Features
391
- - **XSS Protection**: All HTML output is properly escaped
392
+
392
393
  - **ReDoS Protection**: Regular expression constraints have timeout protection
393
394
  - **Secure Constraints**: Type-safe constraint system without code execution
394
395
  - **Thread Safety**: All shared resources are thread-safe
395
396
  - **Input Validation**: Comprehensive parameter validation
396
397
 
397
398
  ### ⚠️ Security Notice
399
+
398
400
  **Proc constraints are deprecated due to security risks** and will be removed in a future version. They allow arbitrary code execution which can be exploited for:
401
+
399
402
  - Code injection attacks
400
403
  - Denial of service attacks
401
404
  - System compromise
402
405
 
403
406
  **Migration Required**: If you're using Proc constraints, please migrate to secure alternatives using our [Migration Guide](MIGRATION_GUIDE.md).
404
407
 
408
+ **Note**: RubyRoutes is a routing library and does not provide application-level security features such as XSS protection, CSRF protection, or authentication. These should be handled by your web framework or additional security middleware.
409
+
405
410
  ## Documentation
406
411
 
407
412
  ### Core Documentation
413
+
408
414
  - **[CONSTRAINTS.md](CONSTRAINTS.md)** - Complete guide to route constraints and security
409
415
  - **[MIGRATION_GUIDE.md](MIGRATION_GUIDE.md)** - Guide for migrating from deprecated Proc constraints
410
- - **[SECURITY_FIXES.md](SECURITY_FIXES.md)** - Details on security improvements
411
416
  - **[USAGE.md](USAGE.md)** - Extended usage scenarios
412
417
 
413
418
  ### Examples
419
+
414
420
  See the `examples/` directory for more detailed examples:
421
+
415
422
  - `examples/basic_usage.rb` - Basic routing examples
416
423
  - `examples/rack_integration.rb` - Full Rack application example
417
424
  - `examples/constraints.rb` - Route constraint examples
@@ -28,7 +28,7 @@ module RubyRoutes
28
28
  # @api internal
29
29
  module Constant
30
30
  # Shared, canonical root path constant (single source of truth).
31
- ROOT_PATH = '/'
31
+ ROOT_PATH = '/'.freeze
32
32
 
33
33
  # Maps a segment's first byte (ASCII) to a Segment class.
34
34
  #
@@ -107,6 +107,8 @@ module RubyRoutes
107
107
  # @return [Integer]
108
108
  QUERY_CACHE_SIZE = 128
109
109
 
110
+ METHOD_CACHE_MAX_SIZE = 1000
111
+
110
112
  # HTTP method constants.
111
113
  HTTP_GET = 'GET'
112
114
  HTTP_POST = 'POST'
@@ -119,7 +121,7 @@ module RubyRoutes
119
121
  # Empty constants for reuse.
120
122
  EMPTY_ARRAY = [].freeze
121
123
  EMPTY_PAIR = [EMPTY_ARRAY, EMPTY_ARRAY].freeze
122
- EMPTY_STRING = ''
124
+ EMPTY_STRING = ''.freeze
123
125
  EMPTY_HASH = {}.freeze
124
126
 
125
127
  # Maximum number of distinct (method, path) composite keys retained
@@ -146,7 +148,7 @@ module RubyRoutes
146
148
  # Default result for no traversal match.
147
149
  #
148
150
  # @return [Array]
149
- NO_TRAVERSAL_RESULT = [nil, false].freeze
151
+ NO_TRAVERSAL_RESULT = [nil, false, EMPTY_HASH].freeze
150
152
 
151
153
  # Built-in validators for constraints.
152
154
  #
@@ -171,7 +173,7 @@ module RubyRoutes
171
173
  segment_string = raw.to_s
172
174
  dispatch_key = segment_string.empty? ? :default : segment_string.getbyte(0)
173
175
  factory = DESCRIPTOR_FACTORIES[dispatch_key] || DESCRIPTOR_FACTORIES[:default]
174
- factory.call(segment_string)
176
+ factory.call(segment_string).freeze
175
177
  end
176
178
  end
177
179
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require_relative 'segment'
4
4
  require_relative 'utility/path_utility'
5
+ require_relative 'utility/method_utility'
6
+ require_relative 'constant'
5
7
 
6
8
  module RubyRoutes
7
9
  # Node
@@ -28,6 +30,7 @@ module RubyRoutes
28
30
  attr_reader :handlers, :static_children
29
31
 
30
32
  include RubyRoutes::Utility::PathUtility
33
+ include RubyRoutes::Utility::MethodUtility
31
34
 
32
35
  def initialize
33
36
  @is_endpoint = false
@@ -44,8 +47,8 @@ module RubyRoutes
44
47
  # @param handler [Object] route or callable
45
48
  # @return [Object] handler
46
49
  def add_handler(method, handler)
47
- method_str = normalize_method(method)
48
- @handlers[method_str] = handler
50
+ method_key = normalize_http_method(method)
51
+ @handlers[method_key] = handler
49
52
  @is_endpoint = true
50
53
  handler
51
54
  end
@@ -55,24 +58,30 @@ module RubyRoutes
55
58
  # @param method [String, Symbol]
56
59
  # @return [Object, nil]
57
60
  def get_handler(method)
58
- @handlers[normalize_method(method)]
61
+ @handlers[normalize_http_method(method)]
59
62
  end
60
63
 
61
64
  # Traverses from this node using a single path segment.
62
- # Returns [next_node_or_nil, stop_traversal(Boolean)].
65
+ # Returns [next_node_or_nil, stop_traversal(Boolean), captured_params(Hash)].
63
66
  #
64
67
  # Optimized + simplified (cyclomatic / perceived complexity, length).
68
+ #
69
+ # @param segment [String] the path segment to match
70
+ # @param index [Integer] the segment index in the full path
71
+ # @param segments [Array<String>] all path segments
72
+ # @param params [Hash] (unused in this method; mutation deferred)
73
+ # @return [Array] [next_node, stop, captured] or NO_TRAVERSAL_RESULT
65
74
  def traverse_for(segment, index, segments, params)
66
- return [@static_children[segment], false] if @static_children[segment]
75
+ return [@static_children[segment], false, {}] if @static_children[segment]
67
76
 
68
77
  if @dynamic_child
69
- capture_dynamic_param(params, @dynamic_child, segment)
70
- return [@dynamic_child, false]
78
+ captured = capture_dynamic_param(@dynamic_child, segment)
79
+ return [@dynamic_child, false, captured]
71
80
  end
72
81
 
73
82
  if @wildcard_child
74
- capture_wildcard_param(params, @wildcard_child, segments, index)
75
- return [@wildcard_child, true]
83
+ captured = capture_wildcard_param(@wildcard_child, segments, index)
84
+ return [@wildcard_child, true, captured]
76
85
  end
77
86
 
78
87
  RubyRoutes::Constant::NO_TRAVERSAL_RESULT
@@ -80,27 +89,27 @@ module RubyRoutes
80
89
 
81
90
  private
82
91
 
83
- # Captures a dynamic parameter value into the params hash if applicable.
92
+ # Captures a dynamic parameter value and returns it for later assignment.
84
93
  #
85
- # @param params [Hash, nil] the parameters hash to update
86
- # @param dyn_node [Node] the dynamic child node
94
+ # @param dynamic_node [Node] the dynamic child node
87
95
  # @param value [String] the segment value to capture
88
- def capture_dynamic_param(params, dyn_node, value)
89
- return unless params && dyn_node.param_name
96
+ # @return [Hash] captured parameter hash or empty hash if no param
97
+ def capture_dynamic_param(dynamic_node, value)
98
+ return {} unless dynamic_node.param_name
90
99
 
91
- params[dyn_node.param_name] = value
100
+ { dynamic_node.param_name => value }
92
101
  end
93
102
 
94
- # Captures a wildcard parameter value into the params hash if applicable.
103
+ # Captures a wildcard parameter value and returns it for later assignment.
95
104
  #
96
- # @param params [Hash, nil] the parameters hash to update
97
105
  # @param wc_node [Node] the wildcard child node
98
106
  # @param segments [Array<String>] the full path segments
99
107
  # @param index [Integer] the current segment index
100
- def capture_wildcard_param(params, wc_node, segments, index)
101
- return unless params && wc_node.param_name
108
+ # @return [Hash] captured parameter hash or empty hash if no param
109
+ def capture_wildcard_param(wildcard_node, segments, index)
110
+ return {} unless wildcard_node.param_name
102
111
 
103
- params[wc_node.param_name] = segments[index..].join('/')
112
+ { wildcard_node.param_name => segments[index..].join('/') }
104
113
  end
105
114
  end
106
115
  end
@@ -4,11 +4,27 @@ require_relative '../constant'
4
4
 
5
5
  module RubyRoutes
6
6
  class RadixTree
7
- # Finder module for traversing the RadixTree and matching routes.
7
+ # Finder module for traversing the RadixTree and finding routes.
8
8
  # Handles path normalization, segment traversal, and parameter extraction.
9
- #
10
- # @module RubyRoutes::RadixTree::Finder
11
9
  module Finder
10
+
11
+ # Evaluate constraint rules for a candidate route.
12
+ #
13
+ # @param route_handler [Object]
14
+ # @param captured_params [Hash]
15
+ # @return [Boolean]
16
+ def check_constraints(route_handler, captured_params)
17
+ return true unless route_handler.respond_to?(:validate_constraints_fast!)
18
+
19
+ begin
20
+ # Use a duplicate to avoid unintended mutation by validators.
21
+ route_handler.validate_constraints_fast!(captured_params)
22
+ true
23
+ rescue RubyRoutes::ConstraintViolation
24
+ false
25
+ end
26
+ end
27
+
12
28
  private
13
29
 
14
30
  # Finds a route handler for the given path and HTTP method.
@@ -17,7 +33,7 @@ module RubyRoutes
17
33
  # @param method_input [String, Symbol] the HTTP method
18
34
  # @param params_out [Hash] optional output hash for captured parameters
19
35
  # @return [Array] [handler, params] or [nil, params] if no match
20
- def find(path_input, method_input, params_out = {})
36
+ def find(path_input, method_input, params_out = nil)
21
37
  path = path_input.to_s
22
38
  method = normalize_http_method(method_input)
23
39
  return root_match(method, params_out) if path.empty? || path == RubyRoutes::Constant::ROOT_PATH
@@ -27,20 +43,23 @@ module RubyRoutes
27
43
 
28
44
  params = params_out || {}
29
45
  state = traversal_state
46
+ captured_params = {}
30
47
 
31
- perform_traversal(segments, state, method, params)
48
+ result = perform_traversal(segments, state, method, params, captured_params)
49
+ return result unless result.nil?
32
50
 
33
- finalize_success(state, method, params)
51
+ finalize_success(state, method, params, captured_params)
34
52
  end
35
53
 
36
54
  # Initializes the traversal state for route matching.
37
55
  #
38
- # @return [Hash] state hash with :current, :best_node, :best_params, :matched
56
+ # @return [Hash] state hash with :current, :best_node, :best_params, :best_captured, :matched
39
57
  def traversal_state
40
58
  {
41
- current: @root_node,
59
+ current: @root,
42
60
  best_node: nil,
43
61
  best_params: nil,
62
+ best_captured: nil,
44
63
  matched: false # Track if any segment was successfully matched
45
64
  }
46
65
  end
@@ -51,16 +70,19 @@ module RubyRoutes
51
70
  # @param state [Hash] traversal state
52
71
  # @param method [String] normalized HTTP method
53
72
  # @param params [Hash] parameters hash
54
- def perform_traversal(segments, state, method, params)
73
+ # @param captured_params [Hash] hash to collect captured parameters
74
+ # @return [nil, Array] nil if traversal succeeds, Array from finalize_on_fail if traversal fails
75
+ def perform_traversal(segments, state, method, params, captured_params)
55
76
  segments.each_with_index do |segment, index|
56
- next_node, stop = traverse_for_segment(state[:current], segment, index, segments, params)
57
- return finalize_on_fail(state, method, params) unless next_node
77
+ next_node, stop = traverse_for_segment(state[:current], segment, index, segments, params, captured_params)
78
+ return finalize_on_fail(state, method, params, captured_params) unless next_node
58
79
 
59
80
  state[:current] = next_node
60
81
  state[:matched] = true # Set matched to true if at least one segment matched
61
- record_candidate(state, method, params) if endpoint_with_method?(state[:current], method)
82
+ record_candidate(state, method, params, captured_params) if endpoint_with_method?(state[:current], method)
62
83
  break if stop
63
84
  end
85
+ nil # Return nil to indicate successful traversal
64
86
  end
65
87
 
66
88
  # Traverses to the next node for a given segment.
@@ -70,9 +92,15 @@ module RubyRoutes
70
92
  # @param index [Integer] segment index
71
93
  # @param segments [Array<String>] all segments
72
94
  # @param params [Hash] parameters hash
73
- # @return [Array] [next_node, stop_traversal]
74
- def traverse_for_segment(node, segment, index, segments, params)
75
- node.traverse_for(segment, index, segments, params)
95
+ # @param captured_params [Hash] hash to collect captured parameters
96
+ # @return [Array] [next_node, stop_traversal, segment_captured]
97
+ def traverse_for_segment(node, segment, index, segments, params, captured_params)
98
+ next_node, stop, segment_captured = node.traverse_for(segment, index, segments, params)
99
+ if segment_captured
100
+ params.merge!(segment_captured) # Merge into running params hash at each step
101
+ captured_params.merge!(segment_captured) # Keep for best candidate consistency
102
+ end
103
+ [next_node, stop]
76
104
  end
77
105
 
78
106
  # Records the current node as a candidate match.
@@ -80,9 +108,11 @@ module RubyRoutes
80
108
  # @param state [Hash] traversal state
81
109
  # @param _method [String] HTTP method (unused)
82
110
  # @param params [Hash] parameters hash
83
- def record_candidate(state, _method, params)
111
+ # @param captured_params [Hash] captured parameters from traversal
112
+ def record_candidate(state, _method, params, captured_params)
84
113
  state[:best_node] = state[:current]
85
114
  state[:best_params] = params.dup
115
+ state[:best_captured] = captured_params.dup
86
116
  end
87
117
 
88
118
  # Checks if the node is an endpoint with a handler for the method.
@@ -99,13 +129,12 @@ module RubyRoutes
99
129
  # @param state [Hash] traversal state
100
130
  # @param method [String] HTTP method
101
131
  # @param params [Hash] parameters hash
132
+ # @param captured_params [Hash] captured parameters from traversal
102
133
  # @return [Array] [handler, params] or [nil, params]
103
- def finalize_on_fail(state, method, params)
104
- if state[:best_node]
105
- handler = state[:best_node].handlers[method]
106
- return constraints_pass?(handler, state[:best_params]) ? [handler, state[:best_params]] : [nil, params]
107
- end
108
- [nil, params]
134
+ def finalize_on_fail(state, method, params, captured_params)
135
+ best_params = state[:best_params] || params
136
+ best_captured = state[:best_captured] || captured_params
137
+ finalize_match(state[:best_node], method, best_params, best_captured)
109
138
  end
110
139
 
111
140
  # Finalizes the result after successful traversal.
@@ -113,15 +142,20 @@ module RubyRoutes
113
142
  # @param state [Hash] traversal state
114
143
  # @param method [String] HTTP method
115
144
  # @param params [Hash] parameters hash
145
+ # @param captured_params [Hash] captured parameters from traversal
116
146
  # @return [Array] [handler, params] or [nil, params]
117
- def finalize_success(state, method, params)
118
- node = state[:current]
119
- if endpoint_with_method?(node, method) && state[:matched]
120
- handler = node.handlers[method]
121
- return [handler, params] if constraints_pass?(handler, params)
147
+ def finalize_success(state, method, params, captured_params)
148
+ result = finalize_match(state[:current], method, params, captured_params)
149
+ return result if result[0]
150
+
151
+ # Try best candidate if current failed
152
+ if state[:best_node]
153
+ best_params = state[:best_params] || params
154
+ best_captured = state[:best_captured] || captured_params
155
+ finalize_match(state[:best_node], method, best_params, best_captured)
156
+ else
157
+ result
122
158
  end
123
- # For non-matching paths, return nil
124
- [nil, params]
125
159
  end
126
160
 
127
161
  # Falls back to the best candidate if no exact match.
@@ -129,35 +163,49 @@ module RubyRoutes
129
163
  # @param state [Hash] traversal state
130
164
  # @param method [String] HTTP method
131
165
  # @param params [Hash] parameters hash
166
+ # @param captured_params [Hash] captured parameters from traversal
132
167
  # @return [Array] [handler, params] or [nil, params]
133
- def fallback_candidate(state, method, params)
134
- if state[:best_node] && state[:best_node] != @root_node
135
- handler = state[:best_node].handlers[method]
136
- return [handler, state[:best_params]] if handler && constraints_pass?(handler, state[:best_params])
168
+
169
+
170
+ # Common method to finalize a match attempt.
171
+ # Assumes the node is already validated as an endpoint.
172
+ #
173
+ # @param node [Node] the node to check for a handler
174
+ # @param method [String] HTTP method
175
+ # @param params [Hash] parameters hash
176
+ # @param captured_params [Hash] captured parameters from traversal
177
+ # @return [Array] [handler, params] or [nil, params]
178
+ def finalize_match(node, method, params, captured_params)
179
+ # Apply captured params once at the beginning
180
+ apply_captured_params(params, captured_params)
181
+
182
+ if node && endpoint_with_method?(node, method)
183
+ handler = node.handlers[method]
184
+ if check_constraints(handler, params)
185
+ return [handler, params]
186
+ end
137
187
  end
188
+ # For non-matching paths, return nil
138
189
  [nil, params]
139
190
  end
140
-
141
- # Handles matching for the root path.
142
191
  #
143
192
  # @param method [String] HTTP method
144
193
  # @param params_out [Hash] parameters hash
145
194
  # @return [Array] [handler, params] or [nil, params]
146
195
  def root_match(method, params_out)
147
- if @root_node.is_endpoint && (handler = @root_node.handlers[method])
196
+ if @root.is_endpoint && (handler = @root.handlers[method])
148
197
  [handler, params_out || {}]
149
198
  else
150
199
  [nil, params_out || {}]
151
200
  end
152
201
  end
153
202
 
154
- # Checks if constraints pass for the handler.
203
+ # Applies captured parameters to the final params hash.
155
204
  #
156
- # @param handler [Object] the route handler
157
- # @param params [Hash] parameters hash
158
- # @return [Boolean] true if constraints pass
159
- def constraints_pass?(handler, params)
160
- check_constraints(handler, params&.dup || {})
205
+ # @param params [Hash] the final parameters hash
206
+ # @param captured_params [Hash] captured parameters from traversal
207
+ def apply_captured_params(params, captured_params)
208
+ params.merge!(captured_params) if captured_params && !captured_params.empty?
161
209
  end
162
210
  end
163
211
  end
@@ -4,8 +4,6 @@ module RubyRoutes
4
4
  class RadixTree
5
5
  # Inserter module for adding routes to the RadixTree.
6
6
  # Handles tokenization, node advancement, and endpoint finalization.
7
- #
8
- # @module RubyRoutes::RadixTree::Inserter
9
7
  module Inserter
10
8
  private
11
9
 
@@ -19,7 +17,7 @@ module RubyRoutes
19
17
  return route_handler if path_string.nil? || path_string.empty?
20
18
 
21
19
  tokens = split_path(path_string)
22
- current_node = @root_node
20
+ current_node = @root
23
21
  tokens.each { |token| current_node = advance_node(current_node, token) }
24
22
  finalize_endpoint(current_node, http_methods, route_handler)
25
23
  route_handler
@@ -48,6 +46,7 @@ module RubyRoutes
48
46
  # @return [Node] the dynamic child node
49
47
  def handle_dynamic(current_node, token)
50
48
  param_name = token[1..]
49
+ raise ArgumentError, "Dynamic parameter name cannot be empty" if param_name.nil? || param_name.empty?
51
50
  current_node.dynamic_child ||= build_param_node(param_name)
52
51
  current_node.dynamic_child
53
52
  end
@@ -52,7 +52,7 @@ module RubyRoutes
52
52
 
53
53
  # Initialize empty tree and split cache.
54
54
  def initialize
55
- @root_node = Node.new
55
+ @root = Node.new
56
56
  @split_cache = RubyRoutes::Route::SmallLru.new(2048)
57
57
  @split_cache_max = 2048
58
58
  @split_cache_order = []
@@ -93,22 +93,5 @@ module RubyRoutes
93
93
  @split_cache.set(raw_path, segments)
94
94
  segments
95
95
  end
96
-
97
- # Evaluate constraint rules for a candidate route.
98
- #
99
- # @param route_handler [Object]
100
- # @param captured_params [Hash]
101
- # @return [Boolean]
102
- def check_constraints(route_handler, captured_params)
103
- return true unless route_handler.respond_to?(:validate_constraints_fast!)
104
-
105
- begin
106
- # Use a duplicate to avoid unintended mutation by validators.
107
- route_handler.validate_constraints_fast!(captured_params)
108
- true
109
- rescue RubyRoutes::ConstraintViolation
110
- false
111
- end
112
- end
113
96
  end
114
97
  end
@@ -20,7 +20,7 @@ module RubyRoutes
20
20
  # @raise [RubyRoutes::ConstraintViolation] If the value is shorter than the minimum length.
21
21
  # @return [void]
22
22
  def check_min_length(constraint, value)
23
- return unless constraint[:min_length] && value.length < constraint[:min_length]
23
+ return unless (min = constraint[:min_length]) && value && value.length < min
24
24
 
25
25
  raise RubyRoutes::ConstraintViolation, "Value too short (minimum #{constraint[:min_length]} characters)"
26
26
  end
@@ -36,7 +36,7 @@ module RubyRoutes
36
36
  # @raise [RubyRoutes::ConstraintViolation] If the value exceeds the maximum length.
37
37
  # @return [void]
38
38
  def check_max_length(constraint, value)
39
- return unless constraint[:max_length] && value.length > constraint[:max_length]
39
+ return unless (max = constraint[:max_length]) && value && value.length > max
40
40
 
41
41
  raise RubyRoutes::ConstraintViolation, "Value too long (maximum #{constraint[:max_length]} characters)"
42
42
  end
@@ -52,7 +52,7 @@ module RubyRoutes
52
52
  # @raise [RubyRoutes::ConstraintViolation] If the value does not match the required format.
53
53
  # @return [void]
54
54
  def check_format(constraint, value)
55
- return unless constraint[:format] && !value.match?(constraint[:format])
55
+ return unless (format = constraint[:format]) && value && !value.match?(format)
56
56
 
57
57
  raise RubyRoutes::ConstraintViolation, 'Value does not match required format'
58
58
  end
@@ -100,9 +100,15 @@ module RubyRoutes
100
100
  # @raise [RubyRoutes::ConstraintViolation] If the value is not in the allowed range.
101
101
  # @return [void]
102
102
  def check_range(constraint, value)
103
- return unless constraint[:range] && !constraint[:range].cover?(value.to_i)
103
+ range = constraint[:range]
104
+ return unless range
105
+ begin
106
+ integer_value = Integer(value) # raises on nil, floats, or junk like "10abc"
107
+ rescue ArgumentError, TypeError
108
+ raise RubyRoutes::ConstraintViolation, 'Value not in allowed range'
109
+ end
104
110
 
105
- raise RubyRoutes::ConstraintViolation, 'Value not in allowed range'
111
+ raise RubyRoutes::ConstraintViolation, 'Value not in allowed range' unless range.cover?(integer_value)
106
112
  end
107
113
  end
108
114
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'timeout'
3
4
  require_relative '../constant'
4
5
 
5
6
  module RubyRoutes
@@ -25,6 +26,7 @@ module RubyRoutes
25
26
 
26
27
  validate_constraint_for(rule, key, params[param_key])
27
28
  end
29
+ nil
28
30
  end
29
31
 
30
32
  # Dispatch a single constraint check.
@@ -51,8 +53,22 @@ module RubyRoutes
51
53
  # @param value [Object] The value to validate.
52
54
  # @return [void]
53
55
  def validate_builtin_constraint(rule, value)
54
- method_sym = RubyRoutes::Constant::BUILTIN_VALIDATORS[rule.to_sym] if rule
55
- send(method_sym, value) if method_sym
56
+ case rule.to_s
57
+ when 'int'
58
+ validate_int_constraint(value)
59
+ when 'uuid'
60
+ validate_uuid_constraint(value)
61
+ when 'email'
62
+ validate_email_constraint(value)
63
+ when 'slug'
64
+ validate_slug_constraint(value)
65
+ when 'alpha'
66
+ validate_alpha_constraint(value)
67
+ when 'alphanumeric'
68
+ validate_alphanumeric_constraint(value)
69
+ else
70
+ invalid!
71
+ end
56
72
  end
57
73
 
58
74
  # Validate a regexp constraint.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'path_generation'
4
4
  require_relative 'warning_helpers'
5
+ require_relative 'constraint_validator'
5
6
 
6
7
  module RubyRoutes
7
8
  class Route
@@ -12,8 +13,6 @@ module RubyRoutes
12
13
  # hashes for performance and includes a 2-slot LRU cache for param key generation.
13
14
  #
14
15
  # Thread-safety: Thread-local storage is used to avoid allocation and cross-thread mutation.
15
- #
16
- # @module RubyRoutes::Route::ParamSupport
17
16
  module ParamSupport
18
17
  include RubyRoutes::Route::WarningHelpers
19
18
 
@@ -127,7 +126,7 @@ module RubyRoutes
127
126
 
128
127
  merge_defaults_fast(params_hash) unless @defaults.empty?
129
128
  validate_constraints_fast!(params_hash) unless @constraints.empty?
130
- params_hash
129
+ params_hash.dup
131
130
  end
132
131
 
133
132
  # Merge query parameters (if any) from full path into param hash.
@@ -3,8 +3,6 @@
3
3
  module RubyRoutes
4
4
  class Route
5
5
  # PathBuilder: generation + segment encoding
6
- #
7
- # @module RubyRoutes::Route::PathBuilder
8
6
  module PathBuilder
9
7
  private
10
8