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.
@@ -5,71 +5,77 @@ module RubyRoutes
5
5
  def initialize
6
6
  @tree = RubyRoutes::RadixTree.new
7
7
  @named_routes = {}
8
- @routes = [] # keep list for specs / iteration / size
9
- @recognition_cache = {} # simple bounded cache: key -> [route, params]
10
- @recognition_cache_order = []
11
- @recognition_cache_max = 4096
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
- # Return the Route object (or nil) to match spec expectations.
23
- handler, _params = @tree.find(request_path, request_method.to_s.upcase)
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] or raise RouteNotFound, "No route named '#{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
- # Normalize method once and attempt recognition cache hit
33
- method_up = request_method.to_s.upcase
34
- cache_key = "#{method_up}:#{request_path}"
35
- if (cached = @recognition_cache[cache_key])
36
- # Return cached params (frozen) directly to avoid heavy dup allocations.
37
- cached_route, cached_params = cached
38
- return { route: cached_route, params: cached_params, controller: cached_route.controller, action: cached_route.action }
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
- # Use a thread-local hash as output for RadixTree to avoid allocating a params Hash
42
- tmp = Thread.current[:ruby_routes_params] ||= {}
43
- handler, _ = @tree.find(request_path, method_up, tmp)
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
- # tmp now contains path params (filled by RadixTree). Merge defaults and query params in-place.
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
- params = tmp.dup
60
+ # Fast path: merge defaults only if they exist
61
+ merge_defaults(route, params) if route.defaults && !route.defaults.empty?
58
62
 
59
- # insert into bounded recognition cache (store frozen params to reduce accidental mutation)
60
- @recognition_cache[cache_key] = [route, params.freeze]
61
- @recognition_cache_order << cache_key
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: 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 = find_named_route(name)
81
- generate_path_from_route(route, params)
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
@@ -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: "#{plural}#index")
43
- get "/#{plural}/new", options.merge(to: "#{plural}#new")
44
- post "/#{plural}", options.merge(to: "#{plural}#create")
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: "#{plural}#show")
48
- get "/#{plural}/:id/edit", options.merge(to: "#{plural}#edit")
49
- put "/#{plural}/:id", options.merge(to: "#{plural}#update")
50
- patch "/#{plural}/:id", options.merge(to: "#{plural}#update")
51
- delete "/#{plural}/:id", options.merge(to: "#{plural}#destroy")
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
- "<a href=\"#{path}\">#{text}</a>"
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
- path = path_to(name, params)
41
- method = params.delete(:method) || :post
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
- html = "<form action=\"#{path}\" method=\"#{method}\">"
44
- html += "<input type=\"hidden\" name=\"_method\" value=\"#{method}\">" if method != :get
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
@@ -1,3 +1,3 @@
1
1
  module RubyRoutes
2
- VERSION = "1.0.0"
2
+ VERSION = "2.0.0"
3
3
  end
data/lib/ruby_routes.rb CHANGED
@@ -11,6 +11,7 @@ module RubyRoutes
11
11
  class Error < StandardError; end
12
12
  class RouteNotFound < Error; end
13
13
  class InvalidRoute < Error; end
14
+ class ConstraintViolation < Error; end
14
15
 
15
16
  # Create a new router instance
16
17
  def self.new(&block)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_routes
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yosef Benny Widyokarsono