ruby_routes 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +12 -5
- data/lib/ruby_routes/constant.rb +3 -3
- data/lib/ruby_routes/node.rb +28 -20
- data/lib/ruby_routes/radix_tree/finder.rb +89 -40
- data/lib/ruby_routes/radix_tree/inserter.rb +1 -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 +16 -2
- data/lib/ruby_routes/route/param_support.rb +1 -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 +6 -3
- data/lib/ruby_routes/route.rb +6 -3
- data/lib/ruby_routes/route_set/cache_helpers.rb +0 -98
- data/lib/ruby_routes/route_set/collection_helpers.rb +9 -11
- data/lib/ruby_routes/route_set.rb +23 -2
- data/lib/ruby_routes/router/build_helpers.rb +0 -1
- data/lib/ruby_routes/router/builder.rb +3 -2
- data/lib/ruby_routes/router/http_helpers.rb +1 -1
- data/lib/ruby_routes/router/scope_helpers.rb +27 -9
- data/lib/ruby_routes/router.rb +3 -1
- 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 +4 -4
- 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: 57b9470b49019746492c10fd0e202c6c544c78459f36e0d2c1f8d400103def93
|
4
|
+
data.tar.gz: 8e59a14f7854cadb955d381fcf7947b7769d0041de5067f3e3dc5827d13cbb26
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c746c95407c222fdd209e464c3f1b89971987d8b41a557b7f173e1acb55fa5d6626b030bab26aed81c7ff766bd9da8c4ba4c2895ce167558a61c089923e14f12
|
7
|
+
data.tar.gz: 6b512f5e810f19fa4031b33f29d3efa72feabb841e8ac3558af67ce6ec43556cacc85577993015cde7f3f400c10e9a36cbc8e007831808a798fa80204fd57046
|
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
|
#
|
@@ -119,7 +119,7 @@ module RubyRoutes
|
|
119
119
|
# Empty constants for reuse.
|
120
120
|
EMPTY_ARRAY = [].freeze
|
121
121
|
EMPTY_PAIR = [EMPTY_ARRAY, EMPTY_ARRAY].freeze
|
122
|
-
EMPTY_STRING = ''
|
122
|
+
EMPTY_STRING = ''.freeze
|
123
123
|
EMPTY_HASH = {}.freeze
|
124
124
|
|
125
125
|
# Maximum number of distinct (method, path) composite keys retained
|
@@ -146,7 +146,7 @@ module RubyRoutes
|
|
146
146
|
# Default result for no traversal match.
|
147
147
|
#
|
148
148
|
# @return [Array]
|
149
|
-
NO_TRAVERSAL_RESULT = [nil, false].freeze
|
149
|
+
NO_TRAVERSAL_RESULT = [nil, false, {}].freeze
|
150
150
|
|
151
151
|
# Built-in validators for constraints.
|
152
152
|
#
|
data/lib/ruby_routes/node.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative 'segment'
|
4
4
|
require_relative 'utility/path_utility'
|
5
|
+
require_relative 'utility/method_utility'
|
5
6
|
|
6
7
|
module RubyRoutes
|
7
8
|
# Node
|
@@ -28,6 +29,7 @@ module RubyRoutes
|
|
28
29
|
attr_reader :handlers, :static_children
|
29
30
|
|
30
31
|
include RubyRoutes::Utility::PathUtility
|
32
|
+
include RubyRoutes::Utility::MethodUtility
|
31
33
|
|
32
34
|
def initialize
|
33
35
|
@is_endpoint = false
|
@@ -44,8 +46,8 @@ module RubyRoutes
|
|
44
46
|
# @param handler [Object] route or callable
|
45
47
|
# @return [Object] handler
|
46
48
|
def add_handler(method, handler)
|
47
|
-
|
48
|
-
@handlers[
|
49
|
+
method_key = normalize_http_method(method)
|
50
|
+
@handlers[method_key] = handler
|
49
51
|
@is_endpoint = true
|
50
52
|
handler
|
51
53
|
end
|
@@ -55,24 +57,30 @@ module RubyRoutes
|
|
55
57
|
# @param method [String, Symbol]
|
56
58
|
# @return [Object, nil]
|
57
59
|
def get_handler(method)
|
58
|
-
@handlers[
|
60
|
+
@handlers[normalize_http_method(method)]
|
59
61
|
end
|
60
62
|
|
61
63
|
# Traverses from this node using a single path segment.
|
62
|
-
# Returns [next_node_or_nil, stop_traversal(Boolean)].
|
64
|
+
# Returns [next_node_or_nil, stop_traversal(Boolean), captured_params(Hash)].
|
63
65
|
#
|
64
66
|
# Optimized + simplified (cyclomatic / perceived complexity, length).
|
67
|
+
#
|
68
|
+
# @param segment [String] the path segment to match
|
69
|
+
# @param index [Integer] the segment index in the full path
|
70
|
+
# @param segments [Array<String>] all path segments
|
71
|
+
# @param params [Hash] (unused in this method; mutation deferred)
|
72
|
+
# @return [Array] [next_node, stop, captured] or NO_TRAVERSAL_RESULT
|
65
73
|
def traverse_for(segment, index, segments, params)
|
66
|
-
return [@static_children[segment], false] if @static_children[segment]
|
74
|
+
return [@static_children[segment], false, {}] if @static_children[segment]
|
67
75
|
|
68
76
|
if @dynamic_child
|
69
|
-
capture_dynamic_param(
|
70
|
-
return [@dynamic_child, false]
|
77
|
+
captured = capture_dynamic_param(@dynamic_child, segment)
|
78
|
+
return [@dynamic_child, false, captured]
|
71
79
|
end
|
72
80
|
|
73
81
|
if @wildcard_child
|
74
|
-
capture_wildcard_param(
|
75
|
-
return [@wildcard_child, true]
|
82
|
+
captured = capture_wildcard_param(@wildcard_child, segments, index)
|
83
|
+
return [@wildcard_child, true, captured]
|
76
84
|
end
|
77
85
|
|
78
86
|
RubyRoutes::Constant::NO_TRAVERSAL_RESULT
|
@@ -80,27 +88,27 @@ module RubyRoutes
|
|
80
88
|
|
81
89
|
private
|
82
90
|
|
83
|
-
# Captures a dynamic parameter value
|
91
|
+
# Captures a dynamic parameter value and returns it for later assignment.
|
84
92
|
#
|
85
|
-
# @param
|
86
|
-
# @param dyn_node [Node] the dynamic child node
|
93
|
+
# @param dynamic_node [Node] the dynamic child node
|
87
94
|
# @param value [String] the segment value to capture
|
88
|
-
|
89
|
-
|
95
|
+
# @return [Hash] captured parameter hash or empty hash if no param
|
96
|
+
def capture_dynamic_param(dynamic_node, value)
|
97
|
+
return {} unless dynamic_node.param_name
|
90
98
|
|
91
|
-
|
99
|
+
{ dynamic_node.param_name => value }
|
92
100
|
end
|
93
101
|
|
94
|
-
# Captures a wildcard parameter value
|
102
|
+
# Captures a wildcard parameter value and returns it for later assignment.
|
95
103
|
#
|
96
|
-
# @param params [Hash, nil] the parameters hash to update
|
97
104
|
# @param wc_node [Node] the wildcard child node
|
98
105
|
# @param segments [Array<String>] the full path segments
|
99
106
|
# @param index [Integer] the current segment index
|
100
|
-
|
101
|
-
|
107
|
+
# @return [Hash] captured parameter hash or empty hash if no param
|
108
|
+
def capture_wildcard_param(wildcard_node, segments, index)
|
109
|
+
return {} unless wildcard_node.param_name
|
102
110
|
|
103
|
-
|
111
|
+
{ wildcard_node.param_name => segments[index..].join('/') }
|
104
112
|
end
|
105
113
|
end
|
106
114
|
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,12 @@ 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
|
+
captured_params.merge!(segment_captured) if segment_captured
|
100
|
+
[next_node, stop]
|
76
101
|
end
|
77
102
|
|
78
103
|
# Records the current node as a candidate match.
|
@@ -80,9 +105,11 @@ module RubyRoutes
|
|
80
105
|
# @param state [Hash] traversal state
|
81
106
|
# @param _method [String] HTTP method (unused)
|
82
107
|
# @param params [Hash] parameters hash
|
83
|
-
|
108
|
+
# @param captured_params [Hash] captured parameters from traversal
|
109
|
+
def record_candidate(state, _method, params, captured_params)
|
84
110
|
state[:best_node] = state[:current]
|
85
111
|
state[:best_params] = params.dup
|
112
|
+
state[:best_captured] = captured_params.dup
|
86
113
|
end
|
87
114
|
|
88
115
|
# Checks if the node is an endpoint with a handler for the method.
|
@@ -99,13 +126,12 @@ module RubyRoutes
|
|
99
126
|
# @param state [Hash] traversal state
|
100
127
|
# @param method [String] HTTP method
|
101
128
|
# @param params [Hash] parameters hash
|
129
|
+
# @param captured_params [Hash] captured parameters from traversal
|
102
130
|
# @return [Array] [handler, params] or [nil, params]
|
103
|
-
def finalize_on_fail(state, method, params)
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
end
|
108
|
-
[nil, params]
|
131
|
+
def finalize_on_fail(state, method, params, captured_params)
|
132
|
+
best_params = state[:best_params] || params
|
133
|
+
best_captured = state[:best_captured] || captured_params
|
134
|
+
finalize_match(state[:best_node], method, best_params, best_captured)
|
109
135
|
end
|
110
136
|
|
111
137
|
# Finalizes the result after successful traversal.
|
@@ -113,15 +139,20 @@ module RubyRoutes
|
|
113
139
|
# @param state [Hash] traversal state
|
114
140
|
# @param method [String] HTTP method
|
115
141
|
# @param params [Hash] parameters hash
|
142
|
+
# @param captured_params [Hash] captured parameters from traversal
|
116
143
|
# @return [Array] [handler, params] or [nil, params]
|
117
|
-
def finalize_success(state, method, params)
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
144
|
+
def finalize_success(state, method, params, captured_params)
|
145
|
+
result = finalize_match(state[:current], method, params, captured_params)
|
146
|
+
return result if result[0]
|
147
|
+
|
148
|
+
# Try best candidate if current failed
|
149
|
+
if state[:best_node]
|
150
|
+
best_params = state[:best_params] || params
|
151
|
+
best_captured = state[:best_captured] || captured_params
|
152
|
+
finalize_match(state[:best_node], method, best_params, best_captured)
|
153
|
+
else
|
154
|
+
result
|
122
155
|
end
|
123
|
-
# For non-matching paths, return nil
|
124
|
-
[nil, params]
|
125
156
|
end
|
126
157
|
|
127
158
|
# Falls back to the best candidate if no exact match.
|
@@ -129,12 +160,31 @@ module RubyRoutes
|
|
129
160
|
# @param state [Hash] traversal state
|
130
161
|
# @param method [String] HTTP method
|
131
162
|
# @param params [Hash] parameters hash
|
163
|
+
# @param captured_params [Hash] captured parameters from traversal
|
132
164
|
# @return [Array] [handler, params] or [nil, params]
|
133
|
-
def fallback_candidate(state, method, params)
|
134
|
-
|
135
|
-
|
136
|
-
|
165
|
+
def fallback_candidate(state, method, params, captured_params)
|
166
|
+
finalize_match(state[:best_node], method, state[:best_params], state[:best_captured])
|
167
|
+
end
|
168
|
+
|
169
|
+
# Common method to finalize a match attempt.
|
170
|
+
# Assumes the node is already validated as an endpoint.
|
171
|
+
#
|
172
|
+
# @param node [Node] the node to check for a handler
|
173
|
+
# @param method [String] HTTP method
|
174
|
+
# @param params [Hash] parameters hash
|
175
|
+
# @param captured_params [Hash] captured parameters from traversal
|
176
|
+
# @return [Array] [handler, params] or [nil, params]
|
177
|
+
def finalize_match(node, method, params, captured_params)
|
178
|
+
if node && endpoint_with_method?(node, method)
|
179
|
+
handler = node.handlers[method]
|
180
|
+
# Apply captured params before constraint validation
|
181
|
+
apply_captured_params(params, captured_params)
|
182
|
+
if check_constraints(handler, params)
|
183
|
+
return [handler, params]
|
184
|
+
end
|
137
185
|
end
|
186
|
+
# For non-matching paths, return nil
|
187
|
+
apply_captured_params(params, captured_params)
|
138
188
|
[nil, params]
|
139
189
|
end
|
140
190
|
|
@@ -144,20 +194,19 @@ module RubyRoutes
|
|
144
194
|
# @param params_out [Hash] parameters hash
|
145
195
|
# @return [Array] [handler, params] or [nil, params]
|
146
196
|
def root_match(method, params_out)
|
147
|
-
if @
|
197
|
+
if @root.is_endpoint && (handler = @root.handlers[method])
|
148
198
|
[handler, params_out || {}]
|
149
199
|
else
|
150
200
|
[nil, params_out || {}]
|
151
201
|
end
|
152
202
|
end
|
153
203
|
|
154
|
-
#
|
204
|
+
# Applies captured parameters to the final params hash.
|
155
205
|
#
|
156
|
-
# @param
|
157
|
-
# @param
|
158
|
-
|
159
|
-
|
160
|
-
check_constraints(handler, params&.dup || {})
|
206
|
+
# @param params [Hash] the final parameters hash
|
207
|
+
# @param captured_params [Hash] captured parameters from traversal
|
208
|
+
def apply_captured_params(params, captured_params)
|
209
|
+
params.merge!(captured_params) if captured_params && !captured_params.empty?
|
161
210
|
end
|
162
211
|
end
|
163
212
|
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
|
@@ -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,20 @@ 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
|
+
end
|
56
70
|
end
|
57
71
|
|
58
72
|
# Validate a regexp constraint.
|
@@ -12,8 +12,6 @@ module RubyRoutes
|
|
12
12
|
# hashes for performance and includes a 2-slot LRU cache for param key generation.
|
13
13
|
#
|
14
14
|
# Thread-safety: Thread-local storage is used to avoid allocation and cross-thread mutation.
|
15
|
-
#
|
16
|
-
# @module RubyRoutes::Route::ParamSupport
|
17
15
|
module ParamSupport
|
18
16
|
include RubyRoutes::Route::WarningHelpers
|
19
17
|
|
@@ -127,7 +125,7 @@ module RubyRoutes
|
|
127
125
|
|
128
126
|
merge_defaults_fast(params_hash) unless @defaults.empty?
|
129
127
|
validate_constraints_fast!(params_hash) unless @constraints.empty?
|
130
|
-
params_hash
|
128
|
+
params_hash.dup
|
131
129
|
end
|
132
130
|
|
133
131
|
# Merge query parameters (if any) from full path into param hash.
|
@@ -40,41 +40,26 @@ module RubyRoutes
|
|
40
40
|
# @return [String] The generated or cached path string.
|
41
41
|
def build_or_fetch_generated_path(merged_params)
|
42
42
|
generation_cache_key = build_generation_cache_key(merged_params)
|
43
|
-
if (cached_path = @gen_cache.get(generation_cache_key))
|
43
|
+
if (cached_path = @cache_mutex.synchronize { @gen_cache.get(generation_cache_key) })
|
44
44
|
return cached_path
|
45
45
|
end
|
46
46
|
|
47
47
|
generated_path = generate_path_string(merged_params)
|
48
|
-
|
49
|
-
|
48
|
+
frozen_path = generated_path.frozen? ? generated_path : generated_path.dup.freeze
|
49
|
+
@cache_mutex.synchronize { @gen_cache.set(generation_cache_key, frozen_path) }
|
50
|
+
frozen_path
|
50
51
|
end
|
51
52
|
|
52
53
|
# Build a generation cache key for merged params.
|
53
54
|
#
|
54
|
-
# This method creates a cache key
|
55
|
-
#
|
55
|
+
# This method creates a cache key from all dynamic path parameters
|
56
|
+
# (required + optional, including splats) present in the merged parameters.
|
56
57
|
#
|
57
58
|
# @param merged_params [Hash] The merged parameters for path generation.
|
58
59
|
# @return [String] The cache key for the generation cache.
|
59
60
|
def build_generation_cache_key(merged_params)
|
60
|
-
@required_params.empty? ?
|
61
|
-
|
62
|
-
|
63
|
-
# Emit deprecation warning for `Proc` constraints once per parameter.
|
64
|
-
#
|
65
|
-
# This method ensures that a deprecation warning for a `Proc` constraint
|
66
|
-
# is only emitted once per parameter. It tracks parameters for which
|
67
|
-
# warnings have already been shown.
|
68
|
-
#
|
69
|
-
# @param param [String, Symbol] The parameter name for which the warning
|
70
|
-
# is being emitted.
|
71
|
-
# @return [void]
|
72
|
-
def warn_proc_constraint_deprecation(param)
|
73
|
-
return if @proc_warnings_shown&.include?(param)
|
74
|
-
|
75
|
-
@proc_warnings_shown ||= Set.new
|
76
|
-
@proc_warnings_shown << param
|
77
|
-
warn_proc_warning(param)
|
61
|
+
names = @required_params.empty? ? @param_names : @required_params
|
62
|
+
names.empty? ? RubyRoutes::Constant::EMPTY_STRING : cache_key_for_params(names, merged_params)
|
78
63
|
end
|
79
64
|
|
80
65
|
# Determine if the route can short-circuit to a static path.
|
@@ -85,7 +70,7 @@ module RubyRoutes
|
|
85
70
|
# @param params [Hash] The parameters for path generation.
|
86
71
|
# @return [Boolean] `true` if the route can short-circuit, `false` otherwise.
|
87
72
|
def static_short_circuit?(params)
|
88
|
-
|
73
|
+
!!@static_path && (params.nil? || params.empty?)
|
89
74
|
end
|
90
75
|
|
91
76
|
# Determine if the route is trivial.
|