ruby_routes 1.0.0 → 2.0.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 +110 -7
- data/lib/ruby_routes/constant.rb +3 -3
- data/lib/ruby_routes/node.rb +29 -20
- data/lib/ruby_routes/radix_tree.rb +94 -45
- data/lib/ruby_routes/route.rb +405 -144
- data/lib/ruby_routes/route_set.rb +122 -40
- data/lib/ruby_routes/router.rb +11 -11
- data/lib/ruby_routes/url_helpers.rb +25 -6
- data/lib/ruby_routes/version.rb +1 -1
- data/lib/ruby_routes.rb +1 -0
- metadata +1 -1
@@ -5,71 +5,77 @@ module RubyRoutes
|
|
5
5
|
def initialize
|
6
6
|
@tree = RubyRoutes::RadixTree.new
|
7
7
|
@named_routes = {}
|
8
|
-
@routes = []
|
9
|
-
|
10
|
-
@
|
11
|
-
@
|
8
|
+
@routes = []
|
9
|
+
# Optimized recognition cache with better data structures
|
10
|
+
@recognition_cache = {}
|
11
|
+
@cache_hits = 0
|
12
|
+
@cache_misses = 0
|
13
|
+
@recognition_cache_max = 8192 # larger for better hit rates
|
12
14
|
end
|
13
15
|
|
14
16
|
def add_route(route)
|
15
17
|
@routes << route
|
16
18
|
@tree.add(route.path, route.methods, route)
|
17
19
|
@named_routes[route.name] = route if route.named?
|
20
|
+
# Clear recognition cache when routes change
|
21
|
+
@recognition_cache.clear if @recognition_cache.size > 100
|
18
22
|
route
|
19
23
|
end
|
20
24
|
|
21
25
|
def find_route(request_method, request_path)
|
22
|
-
#
|
23
|
-
|
26
|
+
# Optimized: avoid repeated string allocation
|
27
|
+
method_up = request_method.to_s.upcase
|
28
|
+
handler, _params = @tree.find(request_path, method_up)
|
24
29
|
handler
|
25
30
|
end
|
26
31
|
|
27
32
|
def find_named_route(name)
|
28
|
-
@named_routes[name]
|
33
|
+
route = @named_routes[name]
|
34
|
+
return route if route
|
35
|
+
raise RouteNotFound, "No route named '#{name}'"
|
29
36
|
end
|
30
37
|
|
31
38
|
def match(request_method, request_path)
|
32
|
-
#
|
33
|
-
method_up = request_method
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
+
# Fast path: normalize method once
|
40
|
+
method_up = method_lookup(request_method)
|
41
|
+
|
42
|
+
# Optimized cache key: avoid string interpolation when possible
|
43
|
+
cache_key = build_cache_key(method_up, request_path)
|
44
|
+
|
45
|
+
# Cache hit: return immediately (cached result includes full structure)
|
46
|
+
if (cached_result = @recognition_cache[cache_key])
|
47
|
+
@cache_hits += 1
|
48
|
+
return cached_result
|
39
49
|
end
|
40
50
|
|
41
|
-
|
42
|
-
|
43
|
-
|
51
|
+
@cache_misses += 1
|
52
|
+
|
53
|
+
# Use thread-local params to avoid allocations
|
54
|
+
params = get_thread_local_params
|
55
|
+
handler, _ = @tree.find(request_path, method_up, params)
|
44
56
|
return nil unless handler
|
45
|
-
route = handler
|
46
57
|
|
47
|
-
|
48
|
-
# defaults first (only set missing keys)
|
49
|
-
if route.defaults
|
50
|
-
route.defaults.each { |k, v| tmp[k] = v unless tmp.key?(k) }
|
51
|
-
end
|
52
|
-
if request_path.include?('?')
|
53
|
-
qp = route.parse_query_params(request_path)
|
54
|
-
qp.each { |k, v| tmp[k] = v } unless qp.empty?
|
55
|
-
end
|
58
|
+
route = handler
|
56
59
|
|
57
|
-
|
60
|
+
# Fast path: merge defaults only if they exist
|
61
|
+
merge_defaults(route, params) if route.defaults && !route.defaults.empty?
|
58
62
|
|
59
|
-
#
|
60
|
-
|
61
|
-
|
62
|
-
if @recognition_cache_order.size > @recognition_cache_max
|
63
|
-
oldest = @recognition_cache_order.shift
|
64
|
-
@recognition_cache.delete(oldest)
|
63
|
+
# Fast path: parse query params only if needed
|
64
|
+
if request_path.include?('?')
|
65
|
+
merge_query_params(route, request_path, params)
|
65
66
|
end
|
66
67
|
|
67
|
-
|
68
|
+
# Create return hash and cache the complete result
|
69
|
+
result_params = params.dup
|
70
|
+
result = {
|
68
71
|
route: route,
|
69
|
-
params:
|
72
|
+
params: result_params,
|
70
73
|
controller: route.controller,
|
71
74
|
action: route.action
|
72
|
-
}
|
75
|
+
}.freeze
|
76
|
+
|
77
|
+
insert_cache_entry(cache_key, result)
|
78
|
+
result
|
73
79
|
end
|
74
80
|
|
75
81
|
def recognize_path(path, method = :get)
|
@@ -77,35 +83,111 @@ module RubyRoutes
|
|
77
83
|
end
|
78
84
|
|
79
85
|
def generate_path(name, params = {})
|
80
|
-
route =
|
81
|
-
|
86
|
+
route = @named_routes[name]
|
87
|
+
if route
|
88
|
+
route.generate_path(params)
|
89
|
+
else
|
90
|
+
raise RouteNotFound, "No route named '#{name}'"
|
91
|
+
end
|
82
92
|
end
|
83
93
|
|
84
94
|
def generate_path_from_route(route, params = {})
|
85
|
-
# Delegate to Route#generate_path which uses precompiled segments + cache
|
86
95
|
route.generate_path(params)
|
87
96
|
end
|
88
97
|
|
89
98
|
def clear!
|
90
99
|
@routes.clear
|
91
100
|
@named_routes.clear
|
101
|
+
@recognition_cache.clear
|
92
102
|
@tree = RadixTree.new
|
103
|
+
@cache_hits = @cache_misses = 0
|
93
104
|
end
|
94
105
|
|
95
106
|
def size
|
96
107
|
@routes.size
|
97
108
|
end
|
109
|
+
alias_method :length, :size
|
98
110
|
|
99
111
|
def empty?
|
100
112
|
@routes.empty?
|
101
113
|
end
|
102
114
|
|
103
115
|
def each(&block)
|
116
|
+
return enum_for(:each) unless block_given?
|
104
117
|
@routes.each(&block)
|
105
118
|
end
|
106
119
|
|
107
120
|
def include?(route)
|
108
121
|
@routes.include?(route)
|
109
122
|
end
|
123
|
+
|
124
|
+
# Performance monitoring
|
125
|
+
def cache_stats
|
126
|
+
total = @cache_hits + @cache_misses
|
127
|
+
hit_rate = total > 0 ? (@cache_hits.to_f / total * 100).round(2) : 0
|
128
|
+
{
|
129
|
+
hits: @cache_hits,
|
130
|
+
misses: @cache_misses,
|
131
|
+
hit_rate: "#{hit_rate}%",
|
132
|
+
size: @recognition_cache.size
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
# Method lookup table to avoid repeated upcasing with interned strings
|
139
|
+
def method_lookup(method)
|
140
|
+
@method_cache ||= Hash.new { |h, k| h[k] = k.to_s.upcase.freeze }
|
141
|
+
@method_cache[method]
|
142
|
+
end
|
143
|
+
|
144
|
+
# Optimized cache key building - avoid string interpolation
|
145
|
+
def build_cache_key(method, path)
|
146
|
+
# Use string interpolation which is faster than buffer + dup + freeze
|
147
|
+
# String interpolation creates a new string directly without intermediate allocations
|
148
|
+
"#{method}:#{path}".freeze
|
149
|
+
end
|
150
|
+
|
151
|
+
# Get thread-local params hash, reusing when possible
|
152
|
+
def get_thread_local_params
|
153
|
+
# Use single thread-local hash that gets cleared, avoiding pool management overhead
|
154
|
+
hash = Thread.current[:ruby_routes_params_hash] ||= {}
|
155
|
+
hash.clear
|
156
|
+
hash
|
157
|
+
end
|
158
|
+
|
159
|
+
def return_params_to_pool(params)
|
160
|
+
# No-op since we're using a single reusable hash per thread
|
161
|
+
end
|
162
|
+
|
163
|
+
# Fast defaults merging
|
164
|
+
def merge_defaults(route, params)
|
165
|
+
route.defaults.each do |key, value|
|
166
|
+
params[key] = value unless params.key?(key)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Fast query params merging
|
171
|
+
def merge_query_params(route, request_path, params)
|
172
|
+
if route.respond_to?(:parse_query_params)
|
173
|
+
qp = route.parse_query_params(request_path)
|
174
|
+
params.merge!(qp) unless qp.empty?
|
175
|
+
elsif route.respond_to?(:query_params)
|
176
|
+
qp = route.query_params(request_path)
|
177
|
+
params.merge!(qp) unless qp.empty?
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Efficient cache insertion with LRU eviction
|
182
|
+
def insert_cache_entry(cache_key, cache_entry)
|
183
|
+
@recognition_cache[cache_key] = cache_entry
|
184
|
+
|
185
|
+
# Simple eviction: clear cache when it gets too large
|
186
|
+
if @recognition_cache.size > @recognition_cache_max
|
187
|
+
# Keep most recently used half
|
188
|
+
keys_to_delete = @recognition_cache.keys[0...(@recognition_cache_max / 2)]
|
189
|
+
keys_to_delete.each { |k| @recognition_cache.delete(k) }
|
190
|
+
end
|
191
|
+
end
|
110
192
|
end
|
111
193
|
end
|
data/lib/ruby_routes/router.rb
CHANGED
@@ -5,6 +5,7 @@ module RubyRoutes
|
|
5
5
|
def initialize(&block)
|
6
6
|
@route_set = RouteSet.new
|
7
7
|
@scope_stack = []
|
8
|
+
@concerns = {}
|
8
9
|
instance_eval(&block) if block_given?
|
9
10
|
end
|
10
11
|
|
@@ -36,19 +37,20 @@ module RubyRoutes
|
|
36
37
|
# Resources routing (Rails-like)
|
37
38
|
def resources(name, options = {}, &block)
|
38
39
|
singular = name.to_s.singularize
|
39
|
-
plural = name.to_s.pluralize
|
40
|
+
plural = (options[:path] || name.to_s.pluralize)
|
41
|
+
controller = options[:controller] || plural
|
40
42
|
|
41
43
|
# Collection routes
|
42
|
-
get "/#{plural}", options.merge(to: "#{
|
43
|
-
get "/#{plural}/new", options.merge(to: "#{
|
44
|
-
post "/#{plural}", options.merge(to: "#{
|
44
|
+
get "/#{plural}", options.merge(to: "#{controller}#index")
|
45
|
+
get "/#{plural}/new", options.merge(to: "#{controller}#new")
|
46
|
+
post "/#{plural}", options.merge(to: "#{controller}#create")
|
45
47
|
|
46
48
|
# Member routes
|
47
|
-
get "/#{plural}/:id", options.merge(to: "#{
|
48
|
-
get "/#{plural}/:id/edit", options.merge(to: "#{
|
49
|
-
put "/#{plural}/:id", options.merge(to: "#{
|
50
|
-
patch "/#{plural}/:id", options.merge(to: "#{
|
51
|
-
delete "/#{plural}/:id", options.merge(to: "#{
|
49
|
+
get "/#{plural}/:id", options.merge(to: "#{controller}#show")
|
50
|
+
get "/#{plural}/:id/edit", options.merge(to: "#{controller}#edit")
|
51
|
+
put "/#{plural}/:id", options.merge(to: "#{controller}#update")
|
52
|
+
patch "/#{plural}/:id", options.merge(to: "#{controller}#update")
|
53
|
+
delete "/#{plural}/:id", options.merge(to: "#{controller}#destroy")
|
52
54
|
|
53
55
|
# Nested resources if specified
|
54
56
|
if options[:nested]
|
@@ -63,7 +65,6 @@ module RubyRoutes
|
|
63
65
|
get "/#{plural}/:id/#{nested_plural}/:nested_id/edit", options.merge(to: "#{nested_plural}#edit")
|
64
66
|
put "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#update")
|
65
67
|
patch "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#update")
|
66
|
-
delete "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#update")
|
67
68
|
delete "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#destroy")
|
68
69
|
end
|
69
70
|
|
@@ -131,7 +132,6 @@ module RubyRoutes
|
|
131
132
|
end
|
132
133
|
|
133
134
|
def concern(name, &block)
|
134
|
-
@concerns ||= {}
|
135
135
|
@concerns[name] = block
|
136
136
|
end
|
137
137
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
1
3
|
module RubyRoutes
|
2
4
|
module UrlHelpers
|
3
5
|
def self.included(base)
|
@@ -33,16 +35,33 @@ module RubyRoutes
|
|
33
35
|
|
34
36
|
def link_to(name, text, params = {})
|
35
37
|
path = path_to(name, params)
|
36
|
-
|
38
|
+
safe_path = CGI.escapeHTML(path.to_s)
|
39
|
+
safe_text = CGI.escapeHTML(text.to_s)
|
40
|
+
"<a href=\"#{safe_path}\">#{safe_text}</a>"
|
37
41
|
end
|
38
42
|
|
39
43
|
def button_to(name, text, params = {})
|
40
|
-
|
41
|
-
method =
|
44
|
+
local_params = params ? params.dup : {}
|
45
|
+
method = local_params.delete(:method) || :post
|
46
|
+
method = method.to_s.downcase
|
47
|
+
path = path_to(name, local_params)
|
48
|
+
|
49
|
+
# HTML forms only support GET and POST
|
50
|
+
# For other methods, use POST with _method hidden field
|
51
|
+
form_method = (method == 'get') ? 'get' : 'post'
|
52
|
+
|
53
|
+
safe_path = CGI.escapeHTML(path.to_s)
|
54
|
+
safe_form_method = CGI.escapeHTML(form_method)
|
55
|
+
html = "<form action=\"#{safe_path}\" method=\"#{safe_form_method}\">"
|
56
|
+
|
57
|
+
# Add _method hidden field for non-GET/POST methods
|
58
|
+
if method != 'get' && method != 'post'
|
59
|
+
safe_method = CGI.escapeHTML(method)
|
60
|
+
html += "<input type=\"hidden\" name=\"_method\" value=\"#{safe_method}\">"
|
61
|
+
end
|
42
62
|
|
43
|
-
|
44
|
-
html += "<
|
45
|
-
html += "<button type=\"submit\">#{text}</button>"
|
63
|
+
safe_text = CGI.escapeHTML(text.to_s)
|
64
|
+
html += "<button type=\"submit\">#{safe_text}</button>"
|
46
65
|
html += "</form>"
|
47
66
|
html
|
48
67
|
end
|
data/lib/ruby_routes/version.rb
CHANGED
data/lib/ruby_routes.rb
CHANGED