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
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'rack'
|
3
|
+
require 'rack/utils'
|
4
4
|
|
5
5
|
module RubyRoutes
|
6
6
|
class Route
|
@@ -43,12 +43,12 @@ module RubyRoutes
|
|
43
43
|
query_part = path[(query_index + 1)..]
|
44
44
|
return RubyRoutes::Constant::EMPTY_HASH if query_part.empty? || query_part.match?(/^\?+$/)
|
45
45
|
|
46
|
-
if (cached_result = @query_cache.get(query_part))
|
46
|
+
if (cached_result = @cache_mutex.synchronize { @query_cache.get(query_part) })
|
47
47
|
return cached_result
|
48
48
|
end
|
49
49
|
|
50
50
|
parsed_result = Rack::Utils.parse_query(query_part)
|
51
|
-
@query_cache.set(query_part, parsed_result)
|
51
|
+
@cache_mutex.synchronize { @query_cache.set(query_part, parsed_result) }
|
52
52
|
parsed_result
|
53
53
|
end
|
54
54
|
end
|
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'set'
|
4
|
+
require_relative '../utility/path_utility'
|
5
|
+
|
3
6
|
module RubyRoutes
|
4
7
|
class Route
|
5
8
|
# SegmentCompiler: path analysis + extraction
|
@@ -8,9 +11,9 @@ module RubyRoutes
|
|
8
11
|
# a route path. It includes utilities for compiling path segments, required
|
9
12
|
# parameters, and static paths, as well as extracting parameters from a
|
10
13
|
# request path.
|
11
|
-
#
|
12
|
-
# @module RubyRoutes::Route::SegmentCompiler
|
13
14
|
module SegmentCompiler
|
15
|
+
include RubyRoutes::Utility::PathUtility
|
16
|
+
|
14
17
|
private
|
15
18
|
|
16
19
|
# Compile the segments from the path.
|
@@ -43,7 +46,7 @@ module RubyRoutes
|
|
43
46
|
dynamic_param_names.freeze
|
44
47
|
else
|
45
48
|
dynamic_param_names.reject do |name|
|
46
|
-
@defaults.key?(name) || @defaults.key?(name.to_sym)
|
49
|
+
@defaults.key?(name) || (@defaults.key?(name.to_sym) if name.is_a?(String))
|
47
50
|
end.freeze
|
48
51
|
end
|
49
52
|
@required_params_set = @required_params.to_set.freeze
|
@@ -39,19 +39,31 @@ module RubyRoutes
|
|
39
39
|
# Validate required parameters once.
|
40
40
|
#
|
41
41
|
# This method validates that all required parameters are present and not
|
42
|
-
# nil. It
|
42
|
+
# nil. It uses per-params caching to avoid re-validation for the same
|
43
|
+
# frozen params.
|
43
44
|
#
|
44
45
|
# @param params [Hash] The parameters to validate.
|
45
46
|
# @raise [RouteNotFound] If required parameters are missing or nil.
|
46
47
|
# @return [void]
|
47
48
|
def validate_required_once(params)
|
48
|
-
return if @required_params.empty?
|
49
|
+
return if @required_params.empty?
|
49
50
|
|
50
|
-
|
51
|
+
# Check cache for existing validation result
|
52
|
+
cached_result = get_cached_validation(params)
|
53
|
+
if cached_result
|
54
|
+
missing, nils = cached_result
|
55
|
+
else
|
56
|
+
# Perform validation
|
57
|
+
missing, nils = validate_required_params(params)
|
58
|
+
# Cache the result only if params are frozen
|
59
|
+
if params.frozen?
|
60
|
+
cache_validation_result(params, [missing, nils])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Raise if invalid
|
51
65
|
raise RouteNotFound, "Missing params: #{missing.join(', ')}" unless missing.empty?
|
52
66
|
raise RouteNotFound, "Missing or nil params: #{nils.join(', ')}" unless nils.empty?
|
53
|
-
|
54
|
-
@required_validated_once = true
|
55
67
|
end
|
56
68
|
|
57
69
|
# Validate required parameters.
|
@@ -64,13 +76,20 @@ module RubyRoutes
|
|
64
76
|
# - `nils` [Array<String>] The keys of parameters that are nil.
|
65
77
|
def validate_required_params(params)
|
66
78
|
return RubyRoutes::Constant::EMPTY_PAIR if @required_params.empty?
|
79
|
+
params ||= {}
|
80
|
+
|
81
|
+
if (cached = get_cached_validation(params))
|
82
|
+
return cached
|
83
|
+
end
|
67
84
|
|
68
85
|
missing = []
|
69
86
|
nils = []
|
70
87
|
@required_params.each do |required_key|
|
71
88
|
process_required_key(required_key, params, missing, nils)
|
72
89
|
end
|
73
|
-
[missing, nils]
|
90
|
+
result = [missing, nils]
|
91
|
+
cache_validation_result(params, result)
|
92
|
+
result
|
74
93
|
end
|
75
94
|
|
76
95
|
# Per-key validation helper used by `validate_required_params`.
|
@@ -85,10 +104,13 @@ module RubyRoutes
|
|
85
104
|
def process_required_key(required_key, params, missing, nils)
|
86
105
|
if params.key?(required_key)
|
87
106
|
nils << required_key if params[required_key].nil?
|
88
|
-
elsif params.key?(symbol_key = required_key.to_sym)
|
89
|
-
nils << required_key if params[symbol_key].nil?
|
90
107
|
else
|
91
|
-
|
108
|
+
symbol_key = required_key.to_sym
|
109
|
+
if params.key?(symbol_key)
|
110
|
+
nils << required_key if params[symbol_key].nil?
|
111
|
+
else
|
112
|
+
missing << required_key
|
113
|
+
end
|
92
114
|
end
|
93
115
|
end
|
94
116
|
|
@@ -104,7 +126,7 @@ module RubyRoutes
|
|
104
126
|
return unless params.frozen?
|
105
127
|
return unless @validation_cache && @validation_cache.size < 64
|
106
128
|
|
107
|
-
@validation_cache.set(params.hash, result)
|
129
|
+
@cache_mutex.synchronize { @validation_cache.set(params.hash, result) }
|
108
130
|
end
|
109
131
|
|
110
132
|
# Fetch cached validation result.
|
@@ -114,7 +136,8 @@ module RubyRoutes
|
|
114
136
|
# @param params [Hash] The parameters used for validation.
|
115
137
|
# @return [Object, nil] The cached validation result, or `nil` if not found.
|
116
138
|
def get_cached_validation(params)
|
117
|
-
@validation_cache
|
139
|
+
return nil unless params && @validation_cache
|
140
|
+
@cache_mutex.synchronize { @validation_cache.get(params.hash) }
|
118
141
|
end
|
119
142
|
|
120
143
|
# Return hash to pool.
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'set'
|
4
|
+
|
3
5
|
module RubyRoutes
|
4
6
|
class Route
|
5
7
|
# WarningHelpers: encapsulate deprecation / warning helpers.
|
@@ -18,11 +20,12 @@ module RubyRoutes
|
|
18
20
|
# is being emitted.
|
19
21
|
# @return [void]
|
20
22
|
def warn_proc_constraint_deprecation(param)
|
21
|
-
|
23
|
+
key = param.to_sym
|
24
|
+
return if @proc_warnings_shown&.include?(key)
|
22
25
|
|
23
26
|
@proc_warnings_shown ||= Set.new
|
24
|
-
@proc_warnings_shown <<
|
25
|
-
warn_proc_warning(
|
27
|
+
@proc_warnings_shown << key
|
28
|
+
warn_proc_warning(key)
|
26
29
|
end
|
27
30
|
|
28
31
|
# Warn about `Proc` constraint deprecation.
|
data/lib/ruby_routes/route.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'uri'
|
4
4
|
require 'timeout'
|
5
5
|
require 'rack'
|
6
|
+
require 'set'
|
6
7
|
require_relative 'constant'
|
7
8
|
require_relative 'node'
|
8
9
|
require_relative 'route/small_lru'
|
@@ -35,9 +36,10 @@ module RubyRoutes
|
|
35
36
|
# - Minimal object allocation in hot paths
|
36
37
|
#
|
37
38
|
# Thread Safety:
|
38
|
-
# - Instance is effectively read‑only after initialization
|
39
|
-
#
|
40
|
-
#
|
39
|
+
# - Instance is effectively read‑only after initialization.
|
40
|
+
# - Internal caches (@query_cache, @gen_cache, @validation_cache) are protected
|
41
|
+
# by a mutex for safe concurrent access across multiple threads.
|
42
|
+
# - Designed for "build during boot, read per request" usage pattern.
|
41
43
|
#
|
42
44
|
# Public API Surface (stable):
|
43
45
|
# - #match?
|
@@ -166,6 +168,7 @@ module RubyRoutes
|
|
166
168
|
@is_resource = @path.match?(%r{/:id(?:$|\.)})
|
167
169
|
@gen_cache = SmallLru.new(512)
|
168
170
|
@query_cache = SmallLru.new(RubyRoutes::Constant::QUERY_CACHE_SIZE)
|
171
|
+
@cache_mutex = Mutex.new # Thread-safe access to caches
|
169
172
|
initialize_validation_cache
|
170
173
|
compile_segments
|
171
174
|
compile_required_params
|
@@ -40,104 +40,6 @@ module RubyRoutes
|
|
40
40
|
@recognition_cache_max = 2048
|
41
41
|
@cache_hits = 0
|
42
42
|
@cache_misses = 0
|
43
|
-
@request_key_pool = {}
|
44
|
-
@request_key_ring = Array.new(RubyRoutes::Constant::REQUEST_KEY_CAPACITY)
|
45
|
-
@entry_count = 0
|
46
|
-
@ring_index = 0
|
47
|
-
end
|
48
|
-
|
49
|
-
# Fetch (or build) a composite request cache key with ring-buffer eviction.
|
50
|
-
#
|
51
|
-
# Ensures consistent use of frozen method/path keys to avoid mixed key space bugs.
|
52
|
-
#
|
53
|
-
# @param http_method [String, Symbol] The HTTP method (e.g., `:get`, `:post`).
|
54
|
-
# @param request_path [String] The request path.
|
55
|
-
# @return [String] The composite request key.
|
56
|
-
def fetch_request_key(http_method, request_path)
|
57
|
-
method_key, path_key = normalize_keys(http_method, request_path)
|
58
|
-
composite_key = build_composite_key(method_key, path_key)
|
59
|
-
|
60
|
-
return composite_key if handle_cache_hit(method_key, path_key, composite_key)
|
61
|
-
|
62
|
-
handle_cache_miss(method_key, path_key, composite_key)
|
63
|
-
composite_key
|
64
|
-
end
|
65
|
-
|
66
|
-
# Normalize keys.
|
67
|
-
#
|
68
|
-
# Converts the HTTP method and request path into frozen strings for consistent
|
69
|
-
# key usage.
|
70
|
-
#
|
71
|
-
# @param http_method [String, Symbol] The HTTP method.
|
72
|
-
# @param request_path [String] The request path.
|
73
|
-
# @return [Array<String>] An array containing the normalized method and path keys.
|
74
|
-
def normalize_keys(http_method, request_path)
|
75
|
-
method_key = http_method.is_a?(String) ? http_method.upcase.freeze : http_method.to_s.upcase.freeze
|
76
|
-
path_key = request_path.is_a?(String) ? request_path.freeze : request_path.to_s.freeze
|
77
|
-
[method_key, path_key]
|
78
|
-
end
|
79
|
-
|
80
|
-
# Build composite key.
|
81
|
-
#
|
82
|
-
# Combines the HTTP method and path into a single composite key.
|
83
|
-
#
|
84
|
-
# @param method_key [String] The normalized HTTP method key.
|
85
|
-
# @param path_key [String] The normalized path key.
|
86
|
-
# @return [String] The composite key.
|
87
|
-
def build_composite_key(method_key, path_key)
|
88
|
-
"#{method_key}:#{path_key}".freeze
|
89
|
-
end
|
90
|
-
|
91
|
-
# Handle cache hit.
|
92
|
-
#
|
93
|
-
# Checks if the composite key already exists in the request key pool.
|
94
|
-
#
|
95
|
-
# @param method_key [String] The normalized HTTP method key.
|
96
|
-
# @param path_key [String] The normalized path key.
|
97
|
-
# @param _composite_key [String] The composite key (unused).
|
98
|
-
# @return [Boolean] `true` if the key exists, `false` otherwise.
|
99
|
-
def handle_cache_hit(method_key, path_key, _composite_key)
|
100
|
-
return true if @request_key_pool[method_key]&.key?(path_key)
|
101
|
-
|
102
|
-
false
|
103
|
-
end
|
104
|
-
|
105
|
-
# Handle cache miss.
|
106
|
-
#
|
107
|
-
# Adds the composite key to the request key pool and manages the ring buffer
|
108
|
-
# for eviction.
|
109
|
-
#
|
110
|
-
# @param method_key [String] The normalized HTTP method key.
|
111
|
-
# @param path_key [String] The normalized path key.
|
112
|
-
# @param composite_key [String] The composite key.
|
113
|
-
# @return [void]
|
114
|
-
def handle_cache_miss(method_key, path_key, composite_key)
|
115
|
-
@request_key_pool[method_key][path_key] = composite_key if @request_key_pool[method_key]
|
116
|
-
@request_key_pool[method_key] = { path_key => composite_key } unless @request_key_pool[method_key]
|
117
|
-
|
118
|
-
if @entry_count < RubyRoutes::Constant::REQUEST_KEY_CAPACITY
|
119
|
-
@request_key_ring[@entry_count] = [method_key, path_key]
|
120
|
-
@entry_count += 1
|
121
|
-
else
|
122
|
-
evict_old_entry(method_key, path_key)
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
# Evict old entry.
|
127
|
-
#
|
128
|
-
# Removes the oldest entry from the request key pool and updates the ring buffer.
|
129
|
-
#
|
130
|
-
# @param method_key [String] The normalized HTTP method key.
|
131
|
-
# @param path_key [String] The normalized path key.
|
132
|
-
# @return [void]
|
133
|
-
def evict_old_entry(method_key, path_key)
|
134
|
-
evict_method, evict_path = @request_key_ring[@ring_index]
|
135
|
-
if (evict_bucket = @request_key_pool[evict_method]) && evict_bucket.delete(evict_path) && evict_bucket.empty?
|
136
|
-
@request_key_pool.delete(evict_method)
|
137
|
-
end
|
138
|
-
@request_key_ring[@ring_index] = [method_key, path_key]
|
139
|
-
@ring_index += 1
|
140
|
-
@ring_index = 0 if @ring_index == RubyRoutes::Constant::REQUEST_KEY_CAPACITY
|
141
43
|
end
|
142
44
|
|
143
45
|
# Fetch cached recognition entry while updating hit counter.
|
@@ -18,9 +18,11 @@ module RubyRoutes
|
|
18
18
|
# @param route [Route] The route to add.
|
19
19
|
# @return [Route] The added route.
|
20
20
|
def add_to_collection(route)
|
21
|
-
@routes << route
|
22
|
-
@radix_tree.add(route.path, route.methods, route)
|
23
21
|
@named_routes[route.name] = route if route.named?
|
22
|
+
@radix_tree.add(route.path, route.methods, route)
|
23
|
+
return route if @routes.include?(route) # Prevent duplicate insertion
|
24
|
+
|
25
|
+
@routes << route
|
24
26
|
route
|
25
27
|
end
|
26
28
|
alias add_route add_to_collection
|
@@ -33,8 +35,7 @@ module RubyRoutes
|
|
33
35
|
# @param route [Route] The route to register.
|
34
36
|
# @return [Route] The registered route.
|
35
37
|
def register(route)
|
36
|
-
(
|
37
|
-
route
|
38
|
+
add_to_collection(route)
|
38
39
|
end
|
39
40
|
|
40
41
|
# Find any route (no params) for a method/path.
|
@@ -55,7 +56,7 @@ module RubyRoutes
|
|
55
56
|
# This method retrieves a route by its name from the named routes collection.
|
56
57
|
# If no route is found, it raises a `RouteNotFound` error.
|
57
58
|
#
|
58
|
-
# @param name [Symbol
|
59
|
+
# @param name [Symbol] The name of the route.
|
59
60
|
# @return [Route] The named route.
|
60
61
|
# @raise [RouteNotFound] If no route with the given name is found.
|
61
62
|
def find_named_route(name)
|
@@ -68,8 +69,8 @@ module RubyRoutes
|
|
68
69
|
# Clear all routes and caches.
|
69
70
|
#
|
70
71
|
# This method clears the internal route collection, named routes, recognition
|
71
|
-
# cache, and radix tree. It also resets cache hit/miss counters and
|
72
|
-
# request key
|
72
|
+
# cache, and radix tree. It also resets cache hit/miss counters and clears
|
73
|
+
# the global request key cache.
|
73
74
|
#
|
74
75
|
# @return [void]
|
75
76
|
def clear!
|
@@ -79,10 +80,7 @@ module RubyRoutes
|
|
79
80
|
@cache_hits = 0
|
80
81
|
@cache_misses = 0
|
81
82
|
@radix_tree = RadixTree.new
|
82
|
-
|
83
|
-
@request_key_ring.fill(nil)
|
84
|
-
@entry_count = 0
|
85
|
-
@ring_index = 0
|
83
|
+
RubyRoutes::Utility::KeyBuilderUtility.clear!
|
86
84
|
end
|
87
85
|
|
88
86
|
# Get the number of routes.
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'radix_tree'
|
3
4
|
require_relative 'utility/key_builder_utility'
|
4
5
|
require_relative 'utility/method_utility'
|
5
6
|
require_relative 'route_set/cache_helpers'
|
@@ -18,7 +19,11 @@ module RubyRoutes
|
|
18
19
|
# a small in‑memory recognition cache.
|
19
20
|
# - Delegate structural path matching to an internal RadixTree.
|
20
21
|
#
|
21
|
-
# Thread
|
22
|
+
# Thread Safety:
|
23
|
+
# - RouteSet instances are not fully thread-safe for modifications.
|
24
|
+
# - Build during boot/initialization, then use read-only per request.
|
25
|
+
# - Global caches (via KeyBuilderUtility) are thread-safe for concurrent reads.
|
26
|
+
# - Per-instance recognition cache is not protected (single-threaded usage assumed).
|
22
27
|
#
|
23
28
|
# @api public (primary integration surface)
|
24
29
|
class RouteSet
|
@@ -85,6 +90,18 @@ module RubyRoutes
|
|
85
90
|
route.generate_path(params)
|
86
91
|
end
|
87
92
|
|
93
|
+
# Replay recorded calls on the router instance.
|
94
|
+
#
|
95
|
+
# This method replays all the recorded route definitions and other
|
96
|
+
# configuration calls on the given router instance.
|
97
|
+
#
|
98
|
+
# @param router [Router] The router instance to replay calls on.
|
99
|
+
# @return [void]
|
100
|
+
def replay_recorded_calls(router)
|
101
|
+
# Placeholder for actual implementation
|
102
|
+
# Iterate over recorded calls and apply them to the router
|
103
|
+
end
|
104
|
+
|
88
105
|
private
|
89
106
|
|
90
107
|
# Set up the radix tree for structural path matching.
|
@@ -117,7 +134,11 @@ module RubyRoutes
|
|
117
134
|
return nil unless matched_route
|
118
135
|
|
119
136
|
# Ensure we have a mutable hash for merging defaults / query params.
|
120
|
-
|
137
|
+
if extracted_params.nil?
|
138
|
+
extracted_params = {}
|
139
|
+
elsif extracted_params.frozen?
|
140
|
+
extracted_params = extracted_params.dup
|
141
|
+
end
|
121
142
|
|
122
143
|
merge_query_params(matched_route, raw_path, extracted_params)
|
123
144
|
merge_defaults(matched_route, extracted_params)
|
@@ -62,8 +62,9 @@ module RubyRoutes
|
|
62
62
|
# `RubyRoutes::Constant::RECORDED_METHODS`. Each method records its
|
63
63
|
# invocation (method name, arguments, and block) in `@recorded_calls`.
|
64
64
|
#
|
65
|
-
#
|
66
|
-
#
|
65
|
+
# The dynamically defined methods accept arbitrary arguments and an optional block,
|
66
|
+
# which are recorded for later processing by the router.
|
67
|
+
#
|
67
68
|
# @return [nil]
|
68
69
|
RubyRoutes::Constant::RECORDED_METHODS.each do |method_name|
|
69
70
|
define_method(method_name) do |*arguments, &definition_block|
|
@@ -108,7 +108,7 @@ module RubyRoutes
|
|
108
108
|
# @raise [RuntimeError] If the router is frozen.
|
109
109
|
# @return [void]
|
110
110
|
def ensure_unfrozen!
|
111
|
-
raise 'Router finalized (immutable)' if @frozen
|
111
|
+
raise 'Router finalized (immutable)' if @frozen || frozen?
|
112
112
|
end
|
113
113
|
|
114
114
|
# Define routes for a singular resource.
|
@@ -61,7 +61,21 @@ module RubyRoutes
|
|
61
61
|
# @param scoped_path [String] The path to prepend the scope's path to.
|
62
62
|
# @return [void]
|
63
63
|
def apply_path_scope(scope, scoped_path)
|
64
|
-
|
64
|
+
path = scope[:path]&.to_s
|
65
|
+
return if path.nil? || path.empty?
|
66
|
+
|
67
|
+
if path.end_with?('/')
|
68
|
+
if scoped_path.start_with?('/')
|
69
|
+
scoped_path.prepend(path.chomp('/'))
|
70
|
+
else
|
71
|
+
scoped_path.prepend(path)
|
72
|
+
end
|
73
|
+
else
|
74
|
+
scoped_path.prepend(scoped_path.start_with?('/') ? path : "#{path}/")
|
75
|
+
end
|
76
|
+
|
77
|
+
# Normalize: Ensure the final path starts with '/'
|
78
|
+
scoped_path.prepend('/') unless scoped_path.start_with?('/')
|
65
79
|
end
|
66
80
|
|
67
81
|
# Apply the module from a scope to the given options.
|
@@ -73,13 +87,17 @@ module RubyRoutes
|
|
73
87
|
# @param scoped_options [Hash] The options to update with the module.
|
74
88
|
# @return [void]
|
75
89
|
def apply_module_scope(scope, scoped_options)
|
76
|
-
|
90
|
+
module_string = scope[:module]&.to_s
|
91
|
+
return if module_string.nil? || module_string.empty?
|
77
92
|
|
78
|
-
if scoped_options[:to]
|
79
|
-
controller, action =
|
80
|
-
|
81
|
-
|
82
|
-
|
93
|
+
if (to_val = scoped_options[:to])
|
94
|
+
controller, action = to_val.to_s.split('#', 2)
|
95
|
+
return if controller.nil? || controller.empty?
|
96
|
+
scoped_options[:to] = action && !action.empty? ? "#{module_string}/#{controller}##{action}" : "#{module_string}/#{controller}"
|
97
|
+
elsif (controller = scoped_options[:controller])
|
98
|
+
controller_string = controller.to_s
|
99
|
+
return if controller_string.empty?
|
100
|
+
scoped_options[:controller] = "#{module_string}/#{controller_string}"
|
83
101
|
end
|
84
102
|
end
|
85
103
|
|
@@ -91,7 +109,7 @@ module RubyRoutes
|
|
91
109
|
def apply_defaults_scope(scope, scoped_options)
|
92
110
|
return unless scope[:defaults]
|
93
111
|
|
94
|
-
scoped_options[:defaults] = (scoped_options[:defaults] || {})
|
112
|
+
scoped_options[:defaults] = scope[:defaults].merge(scoped_options[:defaults] || {})
|
95
113
|
end
|
96
114
|
|
97
115
|
# Apply the constraints from a scope to the given options.
|
@@ -102,7 +120,7 @@ module RubyRoutes
|
|
102
120
|
def apply_constraints_scope(scope, scoped_options)
|
103
121
|
return unless scope[:constraints]
|
104
122
|
|
105
|
-
scoped_options[:constraints] = (scoped_options[:constraints] || {})
|
123
|
+
scoped_options[:constraints] = scope[:constraints].merge(scoped_options[:constraints] || {})
|
106
124
|
end
|
107
125
|
end
|
108
126
|
end
|
data/lib/ruby_routes/router.rb
CHANGED
@@ -3,7 +3,8 @@
|
|
3
3
|
require_relative 'utility/inflector_utility'
|
4
4
|
require_relative 'utility/route_utility'
|
5
5
|
require_relative 'router/http_helpers'
|
6
|
-
|
6
|
+
require_relative 'constant'
|
7
|
+
require_relative 'route_set'
|
7
8
|
module RubyRoutes
|
8
9
|
# RubyRoutes::Router
|
9
10
|
#
|
@@ -117,6 +118,7 @@ module RubyRoutes
|
|
117
118
|
# @return [Router] self.
|
118
119
|
def resources(resource_name, options = {}, &nested_block)
|
119
120
|
define_resource_routes(resource_name, options, &nested_block)
|
121
|
+
self
|
120
122
|
end
|
121
123
|
|
122
124
|
# Define a singular resource.
|
@@ -39,7 +39,9 @@ module RubyRoutes
|
|
39
39
|
def ensure_child(parent_node)
|
40
40
|
parent_node.wildcard_child ||= Node.new
|
41
41
|
wildcard_child_node = parent_node.wildcard_child
|
42
|
-
wildcard_child_node.param_name
|
42
|
+
if wildcard_child_node.param_name.nil?
|
43
|
+
wildcard_child_node.param_name = @param_name
|
44
|
+
end
|
43
45
|
wildcard_child_node
|
44
46
|
end
|
45
47
|
|
@@ -14,9 +14,7 @@ module RubyRoutes
|
|
14
14
|
# Design goals:
|
15
15
|
# - Zero garbage on hot cache hits.
|
16
16
|
# - Bounded memory (REQUEST_KEY_CAPACITY ring).
|
17
|
-
# - Thread
|
18
|
-
#
|
19
|
-
# @module RubyRoutes::Utility::KeyBuilderUtility
|
17
|
+
# - Thread-safe for concurrent access across multiple threads.
|
20
18
|
module KeyBuilderUtility
|
21
19
|
# @!visibility private
|
22
20
|
# { "GET" => { "/users" => "GET:/users" } }
|
@@ -28,12 +26,21 @@ module RubyRoutes
|
|
28
26
|
@ring_index = 0
|
29
27
|
# @!visibility private
|
30
28
|
@entry_count = 0
|
29
|
+
# @!visibility private
|
30
|
+
@mutex = Mutex.new
|
31
31
|
|
32
32
|
class << self
|
33
|
-
#
|
33
|
+
# Clear all cached request keys.
|
34
34
|
#
|
35
|
-
# @return [
|
36
|
-
|
35
|
+
# @return [void]
|
36
|
+
def clear!
|
37
|
+
@mutex.synchronize do
|
38
|
+
@request_key_pool.clear
|
39
|
+
@request_key_ring.fill(nil)
|
40
|
+
@entry_count = 0
|
41
|
+
@ring_index = 0
|
42
|
+
end
|
43
|
+
end
|
37
44
|
|
38
45
|
# Fetch (or create) a frozen "METHOD:PATH" composite key.
|
39
46
|
#
|
@@ -46,12 +53,14 @@ module RubyRoutes
|
|
46
53
|
# @param request_path [String] The request path (e.g., "/users").
|
47
54
|
# @return [String] A frozen canonical key.
|
48
55
|
def fetch_request_key(http_method, request_path)
|
49
|
-
|
56
|
+
@mutex.synchronize do
|
57
|
+
method_key, path_key = prepare_keys(http_method, request_path)
|
50
58
|
|
51
|
-
|
52
|
-
|
59
|
+
bucket = @request_key_pool[method_key] ||= {}
|
60
|
+
return bucket[path_key] if bucket[path_key]
|
53
61
|
|
54
|
-
|
62
|
+
handle_cache_miss(bucket, method_key, path_key)
|
63
|
+
end
|
55
64
|
end
|
56
65
|
|
57
66
|
private
|
@@ -116,7 +125,7 @@ module RubyRoutes
|
|
116
125
|
|
117
126
|
# Build a generic delimited key from components (non‑hot path).
|
118
127
|
#
|
119
|
-
#
|
128
|
+
# Simple join; acceptable for non‑hot paths.
|
120
129
|
#
|
121
130
|
# @param components [Array<#to_s>] The components to join into a key.
|
122
131
|
# @param delimiter [String] The separator (default is ':').
|
@@ -159,8 +168,16 @@ module RubyRoutes
|
|
159
168
|
# @param buffer [String] The buffer to build the key into.
|
160
169
|
# @return [void]
|
161
170
|
def build_param_key_buffer(required_params, merged, buffer)
|
162
|
-
|
163
|
-
|
171
|
+
first = true
|
172
|
+
required_params.each do |param|
|
173
|
+
value = format_param_value(merged[param])
|
174
|
+
if first
|
175
|
+
buffer << value
|
176
|
+
first = false
|
177
|
+
else
|
178
|
+
buffer << '|' << value
|
179
|
+
end
|
180
|
+
end
|
164
181
|
end
|
165
182
|
|
166
183
|
# Format a parameter value for inclusion in the key.
|
@@ -78,9 +78,9 @@ module RubyRoutes
|
|
78
78
|
# @param method_input [String] The HTTP method input.
|
79
79
|
# @return [String] The normalized HTTP method.
|
80
80
|
def normalize_string_method(method_input)
|
81
|
-
|
82
|
-
return key if already_upper_ascii?(key)
|
81
|
+
return method_input if already_upper_ascii?(method_input)
|
83
82
|
|
83
|
+
key = method_input.dup
|
84
84
|
METHOD_CACHE[key] ||= ascii_upcase(key).freeze
|
85
85
|
end
|
86
86
|
|
@@ -98,9 +98,9 @@ module RubyRoutes
|
|
98
98
|
# @return [String] The normalized HTTP method.
|
99
99
|
def normalize_other_method(method_input)
|
100
100
|
coerced = method_input.to_s
|
101
|
-
|
102
|
-
return key if already_upper_ascii?(key)
|
101
|
+
return coerced if already_upper_ascii?(coerced)
|
103
102
|
|
103
|
+
key = coerced.dup
|
104
104
|
METHOD_CACHE[key] ||= ascii_upcase(key).freeze
|
105
105
|
end
|
106
106
|
|
data/lib/ruby_routes/version.rb
CHANGED