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
@@ -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.
|
@@ -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,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'set'
|
4
|
+
require 'thread'
|
5
|
+
|
3
6
|
module RubyRoutes
|
4
7
|
class Route
|
5
8
|
# WarningHelpers: encapsulate deprecation / warning helpers.
|
@@ -17,12 +20,14 @@ module RubyRoutes
|
|
17
20
|
# @param param [String, Symbol] The parameter name for which the warning
|
18
21
|
# is being emitted.
|
19
22
|
# @return [void]
|
20
|
-
def warn_proc_constraint_deprecation(param)
|
21
|
-
return if @proc_warnings_shown&.include?(param)
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
def warn_proc_constraint_deprecation(param)
|
25
|
+
key = param.to_sym
|
26
|
+
@warnings_mutex ||= Mutex.new
|
27
|
+
@warnings_mutex.synchronize do
|
28
|
+
@proc_warnings_shown ||= Set.new
|
29
|
+
warn_proc_warning(key) if @proc_warnings_shown.add?(key)
|
30
|
+
end
|
26
31
|
end
|
27
32
|
|
28
33
|
# 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?
|
@@ -81,7 +83,7 @@ module RubyRoutes
|
|
81
83
|
setup_controller_and_action(options)
|
82
84
|
|
83
85
|
@name = options[:as]
|
84
|
-
@constraints = options[:constraints] || {}
|
86
|
+
@constraints = (options[:constraints] || {}).freeze
|
85
87
|
@defaults = (options[:defaults] || {}).transform_keys(&:to_s).freeze
|
86
88
|
@param_key_slots = [[nil, nil], [nil, nil]]
|
87
89
|
@required_validated_once = false
|
@@ -143,8 +145,9 @@ module RubyRoutes
|
|
143
145
|
# @param options [Hash] The options for the route.
|
144
146
|
# @return [String, nil] The inferred controller name.
|
145
147
|
def extract_controller(options)
|
148
|
+
return options[:controller] if options[:controller]
|
146
149
|
to = options[:to]
|
147
|
-
return
|
150
|
+
return nil unless to
|
148
151
|
|
149
152
|
to.to_s.split('#', 2).first
|
150
153
|
end
|
@@ -166,6 +169,7 @@ module RubyRoutes
|
|
166
169
|
@is_resource = @path.match?(%r{/:id(?:$|\.)})
|
167
170
|
@gen_cache = SmallLru.new(512)
|
168
171
|
@query_cache = SmallLru.new(RubyRoutes::Constant::QUERY_CACHE_SIZE)
|
172
|
+
@cache_mutex = Mutex.new # Thread-safe access to caches
|
169
173
|
initialize_validation_cache
|
170
174
|
compile_segments
|
171
175
|
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.
|
@@ -162,12 +64,15 @@ module RubyRoutes
|
|
162
64
|
# @param entry [Hash] The cache entry.
|
163
65
|
# @return [void]
|
164
66
|
def insert_cache_entry(cache_key, entry)
|
165
|
-
|
166
|
-
|
167
|
-
|
67
|
+
@cache_mutex ||= Mutex.new
|
68
|
+
@cache_mutex.synchronize do
|
69
|
+
if @recognition_cache.size >= @recognition_cache_max
|
70
|
+
@recognition_cache.keys.first(@recognition_cache_max / 4).each do |evict_key|
|
71
|
+
@recognition_cache.delete(evict_key)
|
72
|
+
end
|
168
73
|
end
|
74
|
+
@recognition_cache[cache_key] = entry
|
169
75
|
end
|
170
|
-
@recognition_cache[cache_key] = entry
|
171
76
|
end
|
172
77
|
end
|
173
78
|
end
|
@@ -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
|
22
|
-
|
21
|
+
return route if @routes.include?(route) # Prevent duplicate insertion
|
22
|
+
|
23
23
|
@named_routes[route.name] = route if route.named?
|
24
|
+
@radix_tree.add(route.path, route.methods, route)
|
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,21 +69,21 @@ 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!
|
76
|
-
@
|
77
|
-
@
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
77
|
+
@cache_mutex ||= Mutex.new
|
78
|
+
@cache_mutex.synchronize do
|
79
|
+
@routes.clear
|
80
|
+
@named_routes.clear
|
81
|
+
@recognition_cache.clear
|
82
|
+
@cache_hits = 0
|
83
|
+
@cache_misses = 0
|
84
|
+
@radix_tree = RadixTree.new
|
85
|
+
RubyRoutes::Utility::KeyBuilderUtility.clear!
|
86
|
+
end
|
86
87
|
end
|
87
88
|
|
88
89
|
# 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
|
@@ -117,7 +122,11 @@ module RubyRoutes
|
|
117
122
|
return nil unless matched_route
|
118
123
|
|
119
124
|
# Ensure we have a mutable hash for merging defaults / query params.
|
120
|
-
|
125
|
+
if extracted_params.nil?
|
126
|
+
extracted_params = {}
|
127
|
+
elsif extracted_params.frozen?
|
128
|
+
extracted_params = extracted_params.dup
|
129
|
+
end
|
121
130
|
|
122
131
|
merge_query_params(matched_route, raw_path, extracted_params)
|
123
132
|
merge_defaults(matched_route, extracted_params)
|
@@ -17,7 +17,12 @@ module RubyRoutes
|
|
17
17
|
def build
|
18
18
|
router = Router.new
|
19
19
|
validate_calls(@recorded_calls)
|
20
|
-
|
20
|
+
RubyRoutes::Constant::RECORDED_METHODS.each do |method_name|
|
21
|
+
define_method(method_name) do |*arguments, &definition_block|
|
22
|
+
@recorded_calls << [__method__, arguments, definition_block]
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
end
|
21
26
|
router.finalize!
|
22
27
|
router
|
23
28
|
end
|
@@ -42,7 +42,10 @@ module RubyRoutes
|
|
42
42
|
# @return [Array<Array(Symbol, Array, Proc|NilClass)>]
|
43
43
|
# A snapshot of the recorded calls to avoid external mutation.
|
44
44
|
def recorded_calls
|
45
|
-
|
45
|
+
# Deep-copy each recorded call’s args array and freeze the result to prevent mutation
|
46
|
+
@recorded_calls
|
47
|
+
.map { |(method_name, args, block)| [method_name, args.dup.freeze, block] }
|
48
|
+
.freeze
|
46
49
|
end
|
47
50
|
|
48
51
|
# Initialize the Builder.
|
@@ -62,8 +65,9 @@ module RubyRoutes
|
|
62
65
|
# `RubyRoutes::Constant::RECORDED_METHODS`. Each method records its
|
63
66
|
# invocation (method name, arguments, and block) in `@recorded_calls`.
|
64
67
|
#
|
65
|
-
#
|
66
|
-
#
|
68
|
+
# The dynamically defined methods accept arbitrary arguments and an optional block,
|
69
|
+
# which are recorded for later processing by the router.
|
70
|
+
#
|
67
71
|
# @return [nil]
|
68
72
|
RubyRoutes::Constant::RECORDED_METHODS.each do |method_name|
|
69
73
|
define_method(method_name) do |*arguments, &definition_block|
|
@@ -71,26 +75,6 @@ module RubyRoutes
|
|
71
75
|
nil
|
72
76
|
end
|
73
77
|
end
|
74
|
-
|
75
|
-
private
|
76
|
-
|
77
|
-
# Validate the recorded calls.
|
78
|
-
#
|
79
|
-
# This method ensures that all recorded calls use valid router methods
|
80
|
-
# as defined in `RubyRoutes::Constant::RECORDED_METHODS`.
|
81
|
-
#
|
82
|
-
# @param recorded_calls [Array<Array(Symbol, Array, Proc|NilClass)>]
|
83
|
-
# The recorded calls to validate.
|
84
|
-
# @raise [ArgumentError] If any recorded call uses an invalid method.
|
85
|
-
# @return [void]
|
86
|
-
def validate_calls(recorded_calls)
|
87
|
-
allowed_router_methods = RubyRoutes::Constant::RECORDED_METHODS
|
88
|
-
recorded_calls.each do |(router_method, _arguments, _definition_block)|
|
89
|
-
unless router_method.is_a?(Symbol) && allowed_router_methods.include?(router_method)
|
90
|
-
raise ArgumentError, "Invalid router method: #{router_method.inspect}"
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
78
|
end
|
95
79
|
end
|
96
80
|
end
|
@@ -83,7 +83,12 @@ module RubyRoutes
|
|
83
83
|
via = options[:via]
|
84
84
|
raise ArgumentError, 'match requires :via (e.g., via: [:get, :post])' if via.nil? || Array(via).empty?
|
85
85
|
|
86
|
-
|
86
|
+
normalized_via = Array(via)
|
87
|
+
opts = options.dup
|
88
|
+
# Keep :via in opts only if BuildHelpers expects it; otherwise delete.
|
89
|
+
# If BuildHelpers infers from the second arg, delete it:
|
90
|
+
opts.delete(:via)
|
91
|
+
add_route(path, build_route_options(opts, normalized_via))
|
87
92
|
self
|
88
93
|
end
|
89
94
|
|
@@ -108,7 +113,7 @@ module RubyRoutes
|
|
108
113
|
# @raise [RuntimeError] If the router is frozen.
|
109
114
|
# @return [void]
|
110
115
|
def ensure_unfrozen!
|
111
|
-
raise 'Router finalized (immutable)' if @frozen
|
116
|
+
raise 'Router finalized (immutable)' if @frozen || frozen?
|
112
117
|
end
|
113
118
|
|
114
119
|
# Define routes for a singular resource.
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'build_helpers'
|
4
|
+
require_relative '../utility/inflector_utility'
|
4
5
|
|
5
6
|
module RubyRoutes
|
6
7
|
class Router
|
@@ -13,8 +14,6 @@ module RubyRoutes
|
|
13
14
|
module ResourceHelpers
|
14
15
|
include RubyRoutes::Router::BuildHelpers
|
15
16
|
|
16
|
-
private
|
17
|
-
|
18
17
|
# Define RESTful routes for a resource.
|
19
18
|
#
|
20
19
|
# @param resource_name [Symbol, String] The name of the resource.
|
@@ -35,6 +34,8 @@ module RubyRoutes
|
|
35
34
|
end
|
36
35
|
end
|
37
36
|
|
37
|
+
private
|
38
|
+
|
38
39
|
# Prepare options by removing the `:to` key if present.
|
39
40
|
#
|
40
41
|
# @param options [Hash] The options hash.
|