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
@@ -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,9 @@
|
|
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 'router/resource_helpers'
|
7
|
+
require_relative 'constant'
|
8
|
+
require_relative 'route_set'
|
7
9
|
module RubyRoutes
|
8
10
|
# RubyRoutes::Router
|
9
11
|
#
|
@@ -86,11 +88,16 @@ module RubyRoutes
|
|
86
88
|
return self if @frozen
|
87
89
|
|
88
90
|
@frozen = true
|
91
|
+
if @route_set.respond_to?(:finalize!)
|
92
|
+
@route_set.finalize!
|
93
|
+
else
|
94
|
+
@route_set.freeze
|
95
|
+
end
|
96
|
+
@route_utils.freeze if @route_utils.respond_to?(:freeze)
|
89
97
|
@scope_stack.freeze
|
90
98
|
@concerns.freeze
|
91
99
|
self
|
92
100
|
end
|
93
|
-
|
94
101
|
# Check if the router is frozen.
|
95
102
|
#
|
96
103
|
# @return [Boolean] `true` if the router is frozen, `false` otherwise.
|
@@ -98,12 +105,17 @@ module RubyRoutes
|
|
98
105
|
!!@frozen
|
99
106
|
end
|
100
107
|
|
101
|
-
# Define
|
108
|
+
# Define the root route.
|
102
109
|
#
|
103
110
|
# @param options [Hash] The options for the root route.
|
104
111
|
# @return [Router] self.
|
105
112
|
def root(options = {})
|
106
|
-
|
113
|
+
ensure_unfrozen!
|
114
|
+
if options[:to]
|
115
|
+
add_route('/', via: [:get], **options)
|
116
|
+
else
|
117
|
+
add_route('/', controller: 'root', action: :index, via: [:get], **options)
|
118
|
+
end
|
107
119
|
self
|
108
120
|
end
|
109
121
|
|
@@ -116,7 +128,9 @@ module RubyRoutes
|
|
116
128
|
# @param nested_block [Proc] The block for nested routes.
|
117
129
|
# @return [Router] self.
|
118
130
|
def resources(resource_name, options = {}, &nested_block)
|
131
|
+
ensure_unfrozen!
|
119
132
|
define_resource_routes(resource_name, options, &nested_block)
|
133
|
+
self
|
120
134
|
end
|
121
135
|
|
122
136
|
# Define a singular resource.
|
@@ -125,6 +139,7 @@ module RubyRoutes
|
|
125
139
|
# @param options [Hash] The options for the resource.
|
126
140
|
# @return [Router] self.
|
127
141
|
def resource(resource_name, options = {})
|
142
|
+
ensure_unfrozen!
|
128
143
|
singular = RubyRoutes::Utility::InflectorUtility.singularize(resource_name.to_s)
|
129
144
|
controller = options[:controller] || singular
|
130
145
|
define_singular_routes(singular, controller, options)
|
@@ -139,6 +154,7 @@ module RubyRoutes
|
|
139
154
|
# @param block [Proc] The block for nested routes.
|
140
155
|
# @return [Router] self.
|
141
156
|
def namespace(namespace_name, options = {}, &block)
|
157
|
+
ensure_unfrozen!
|
142
158
|
push_scope({ path: "/#{namespace_name}", module: namespace_name }.merge(options)) do
|
143
159
|
instance_eval(&block) if block
|
144
160
|
end
|
@@ -150,6 +166,7 @@ module RubyRoutes
|
|
150
166
|
# @param block [Proc] The block for nested routes.
|
151
167
|
# @return [Router] self.
|
152
168
|
def scope(options_or_path = {}, &block)
|
169
|
+
ensure_unfrozen!
|
153
170
|
scope_entry = options_or_path.is_a?(String) ? { path: options_or_path } : options_or_path
|
154
171
|
push_scope(scope_entry) { instance_eval(&block) if block }
|
155
172
|
end
|
@@ -160,6 +177,7 @@ module RubyRoutes
|
|
160
177
|
# @param block [Proc] The block for nested routes.
|
161
178
|
# @return [Router] self.
|
162
179
|
def constraints(constraints_hash = {}, &block)
|
180
|
+
ensure_unfrozen!
|
163
181
|
push_scope(constraints: constraints_hash) { instance_eval(&block) if block }
|
164
182
|
end
|
165
183
|
|
@@ -169,6 +187,7 @@ module RubyRoutes
|
|
169
187
|
# @param block [Proc] The block for nested routes.
|
170
188
|
# @return [Router] self.
|
171
189
|
def defaults(defaults_hash = {}, &block)
|
190
|
+
ensure_unfrozen!
|
172
191
|
push_scope(defaults: defaults_hash) { instance_eval(&block) if block }
|
173
192
|
end
|
174
193
|
|
@@ -190,6 +209,7 @@ module RubyRoutes
|
|
190
209
|
# @param block [Proc] The block for additional routes.
|
191
210
|
# @return [void]
|
192
211
|
def concerns(*concern_names, &block)
|
212
|
+
ensure_unfrozen!
|
193
213
|
concern_names.each do |name|
|
194
214
|
concern_block = @concerns[name]
|
195
215
|
raise "Concern '#{name}' not found" unless concern_block
|
@@ -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.
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative '../constant'
|
4
|
+
require_relative '../route/small_lru'
|
4
5
|
|
5
6
|
module RubyRoutes
|
6
7
|
module Utility
|
@@ -38,15 +39,10 @@ module RubyRoutes
|
|
38
39
|
}.freeze
|
39
40
|
|
40
41
|
# Cache for non‑predefined or previously seen method tokens.
|
41
|
-
#
|
42
|
-
# Values: frozen uppercase `String`
|
42
|
+
# Now uses SmallLru for LRU eviction instead of simple clearing.
|
43
43
|
#
|
44
|
-
#
|
45
|
-
|
46
|
-
# @return [Hash{(String,Symbol) => String}]
|
47
|
-
# rubocop:disable Style/MutableConstant
|
48
|
-
METHOD_CACHE = {} # Intentionally mutable for caching
|
49
|
-
# rubocop:enable Style/MutableConstant
|
44
|
+
# @return [SmallLru]
|
45
|
+
METHOD_CACHE = RubyRoutes::Route::SmallLru.new(RubyRoutes::Constant::METHOD_CACHE_MAX_SIZE)
|
50
46
|
|
51
47
|
# Normalize an HTTP method‑like input to a canonical uppercase `String`.
|
52
48
|
#
|
@@ -78,10 +74,11 @@ module RubyRoutes
|
|
78
74
|
# @param method_input [String] The HTTP method input.
|
79
75
|
# @return [String] The normalized HTTP method.
|
80
76
|
def normalize_string_method(method_input)
|
81
|
-
|
82
|
-
return key if already_upper_ascii?(key)
|
77
|
+
return method_input if already_upper_ascii?(method_input)
|
83
78
|
|
84
|
-
|
79
|
+
# Use SmallLru for LRU eviction, freeze key to prevent mutation
|
80
|
+
key = method_input.dup.freeze
|
81
|
+
METHOD_CACHE.get(key) || METHOD_CACHE.set(key, ascii_upcase(method_input.dup).freeze)
|
85
82
|
end
|
86
83
|
|
87
84
|
# Normalize a `Symbol` HTTP method.
|
@@ -89,7 +86,10 @@ module RubyRoutes
|
|
89
86
|
# @param method_input [Symbol] The HTTP method input.
|
90
87
|
# @return [String] The normalized HTTP method.
|
91
88
|
def normalize_symbol_method(method_input)
|
92
|
-
SYMBOL_MAP[method_input] ||
|
89
|
+
SYMBOL_MAP[method_input] || begin
|
90
|
+
key = method_input.to_s.freeze
|
91
|
+
METHOD_CACHE.get(key) || METHOD_CACHE.set(key, ascii_upcase(method_input.to_s).freeze)
|
92
|
+
end
|
93
93
|
end
|
94
94
|
|
95
95
|
# Normalize an arbitrary HTTP method input.
|
@@ -98,10 +98,11 @@ 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
|
|
104
|
-
|
103
|
+
# Use SmallLru for LRU eviction, freeze key to prevent mutation
|
104
|
+
key = coerced.dup.freeze
|
105
|
+
METHOD_CACHE.get(key) || METHOD_CACHE.set(key, ascii_upcase(coerced.dup).freeze)
|
105
106
|
end
|
106
107
|
|
107
108
|
# Determine if a `String` consists solely of uppercase ASCII (`A–Z`) or non‑letters.
|
data/lib/ruby_routes/version.rb
CHANGED