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 +4 -4
- data/README.md +12 -5
- data/lib/ruby_routes/constant.rb +6 -4
- data/lib/ruby_routes/node.rb +29 -20
- data/lib/ruby_routes/radix_tree/finder.rb +90 -42
- data/lib/ruby_routes/radix_tree/inserter.rb +2 -3
- data/lib/ruby_routes/radix_tree.rb +1 -18
- data/lib/ruby_routes/route/check_helpers.rb +11 -5
- data/lib/ruby_routes/route/constraint_validator.rb +18 -2
- data/lib/ruby_routes/route/param_support.rb +2 -3
- data/lib/ruby_routes/route/path_builder.rb +0 -2
- data/lib/ruby_routes/route/path_generation.rb +9 -24
- data/lib/ruby_routes/route/query_helpers.rb +3 -3
- data/lib/ruby_routes/route/segment_compiler.rb +6 -3
- data/lib/ruby_routes/route/validation_helpers.rb +34 -11
- data/lib/ruby_routes/route/warning_helpers.rb +10 -5
- data/lib/ruby_routes/route.rb +9 -5
- data/lib/ruby_routes/route_set/cache_helpers.rb +7 -102
- data/lib/ruby_routes/route_set/collection_helpers.rb +18 -17
- data/lib/ruby_routes/route_set.rb +11 -2
- data/lib/ruby_routes/router/build_helpers.rb +6 -1
- data/lib/ruby_routes/router/builder.rb +7 -23
- data/lib/ruby_routes/router/http_helpers.rb +7 -2
- data/lib/ruby_routes/router/resource_helpers.rb +3 -2
- data/lib/ruby_routes/router/scope_helpers.rb +27 -9
- data/lib/ruby_routes/router.rb +24 -4
- data/lib/ruby_routes/segments/wildcard_segment.rb +3 -1
- data/lib/ruby_routes/utility/key_builder_utility.rb +30 -13
- data/lib/ruby_routes/utility/method_utility.rb +16 -15
- data/lib/ruby_routes/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 07fa3c9d3d13ac8b73c6f4775871cd5ad6c3cd7c5d0e571797b83fb0dea72189
|
4
|
+
data.tar.gz: 2684722163772fe4489d4e8c32eaff036002c5b58e05be68e25e9a0c59cd680b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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**:
|
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.
|
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
|
-
- **
|
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
|
-
|
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
|
data/lib/ruby_routes/constant.rb
CHANGED
@@ -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
|
data/lib/ruby_routes/node.rb
CHANGED
@@ -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
|
-
|
48
|
-
@handlers[
|
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[
|
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(
|
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(
|
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
|
92
|
+
# Captures a dynamic parameter value and returns it for later assignment.
|
84
93
|
#
|
85
|
-
# @param
|
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
|
-
|
89
|
-
|
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
|
-
|
100
|
+
{ dynamic_node.param_name => value }
|
92
101
|
end
|
93
102
|
|
94
|
-
# Captures a wildcard parameter value
|
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
|
-
|
101
|
-
|
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
|
-
|
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
|
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: @
|
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
|
-
|
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
|
-
# @
|
74
|
-
|
75
|
-
|
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
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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 @
|
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
|
-
#
|
203
|
+
# Applies captured parameters to the final params hash.
|
155
204
|
#
|
156
|
-
# @param
|
157
|
-
# @param
|
158
|
-
|
159
|
-
|
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 = @
|
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
|
-
@
|
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 <
|
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 >
|
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?(
|
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
|
-
|
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
|
-
|
55
|
-
|
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.
|