Class: ActionController::Routing::RouteSet
- Defined in:
- lib/action_controller/routing/route_set.rb,
lib/action_controller/routing/recognition_optimisation.rb
Overview
BEFORE: 0.191446860631307 ms/url AFTER: 0.029847304022858 ms/url Speed up: 6.4 times
Route recognition is slow due to one-by-one iterating over a whole routeset (each map.resources generates at least 14 routes) and matching weird regexps on each step.
We optimize this by skipping all URI segments that 100% sure can’t be matched, moving deeper in a tree of routes (where node == segment) until first possible match is accured. In such case, we start walking a flat list of routes, matching them with accurate matcher. So, first step: search a segment tree for the first relevant index. Second step: iterate routes starting with that index.
How tree is walked? We can do a recursive tests, but it’s smarter: We just create a tree of if-s and elsif-s matching segments.
We have segments of 3 flavors: 1) nil (no segment, route finished) 2) const-dot-dynamic (like “/posts.:xml”, “/preview.:size.jpg”) 3) const (like “/posts”, “/comments”) 4) dynamic (“/:id”, “file.:size.:extension”)
We split incoming string into segments and iterate over them. When segment is nil, we drop immediately, on a current node index. When segment is equal to some const, we step into branch. If none constants matched, we step into ‘dynamic’ branch (it’s a last). If we can’t match anything, we drop to last index on a level.
Note: we maintain the original routes order, so we finish building
steps on a first dynamic segment.
Example. Given the routes:
0 /posts/
1 /posts/:id
2 /posts/:id/comments
3 /posts/blah
4 /users/
5 /users/:id
6 /users/:id/profile
request_uri = /users/123
There will be only 4 iterations:
1) segm test for /posts prefix, skip all /posts/* routes
2) segm test for /users/
3) segm test for /users/:id
(jump to list index = 5)
4) full test for /users/:id => here we are!
Defined Under Namespace
Classes: Mapper, NamedRouteCollection
Instance Attribute Summary collapse
-
#configuration_file ⇒ Object
Returns the value of attribute configuration_file.
-
#named_routes ⇒ Object
Returns the value of attribute named_routes.
-
#routes ⇒ Object
Returns the value of attribute routes.
Instance Method Summary collapse
- #add_named_route(name, path, options = {}) ⇒ Object
- #add_route(path, options = {}) ⇒ Object
- #build_expiry(options, recall) ⇒ Object
-
#builder ⇒ Object
Subclasses and plugins may override this method to specify a different RouteBuilder instance, so that other route DSL’s can be created.
- #clear! ⇒ Object
- #draw {|Mapper.new(self)| ... } ⇒ Object
- #empty? ⇒ Boolean
-
#extra_keys(options, recall = {}) ⇒ Object
Generate the path indicated by the arguments, and return an array of the keys that were not used to generate it.
-
#extract_request_environment(request) ⇒ Object
Subclasses and plugins may override this method to extract further attributes from the request, for use by route conditions and such.
- #generate(options, recall = {}, method = :generate) ⇒ Object
- #generate_code(list, padding = ' ', level = 0) ⇒ Object
- #generate_extras(options, recall = {}) ⇒ Object
-
#initialize ⇒ RouteSet
constructor
A new instance of RouteSet.
- #install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) ⇒ Object
- #load! ⇒ Object (also: #reload!)
- #load_routes! ⇒ Object
- #options_as_params(options) ⇒ Object
-
#raise_named_route_error(options, named_route, named_route_name) ⇒ Object
try to give a helpful error message when named route generation fails.
- #recognize(request) ⇒ Object
- #recognize_path(path, environment = {}) ⇒ Object
- #reload ⇒ Object
- #routes_by_controller ⇒ Object
- #routes_for(options, merged, expire_on) ⇒ Object
- #routes_for_controller_and_action(controller, action) ⇒ Object
- #routes_for_controller_and_action_and_keys(controller, action, keys) ⇒ Object
- #segment_tree(routes) ⇒ Object
-
#to_plain_segments(str) ⇒ Object
this must be really fast.
Constructor Details
#initialize ⇒ RouteSet
Returns a new instance of RouteSet.
199 200 201 202 203 204 |
# File 'lib/action_controller/routing/route_set.rb', line 199 def initialize self.routes = [] self.named_routes = NamedRouteCollection.new clear_recognize_optimized! end |
Instance Attribute Details
#configuration_file ⇒ Object
Returns the value of attribute configuration_file.
197 198 199 |
# File 'lib/action_controller/routing/route_set.rb', line 197 def configuration_file @configuration_file end |
#named_routes ⇒ Object
Returns the value of attribute named_routes.
197 198 199 |
# File 'lib/action_controller/routing/route_set.rb', line 197 def named_routes @named_routes end |
#routes ⇒ Object
Returns the value of attribute routes.
197 198 199 |
# File 'lib/action_controller/routing/route_set.rb', line 197 def routes @routes end |
Instance Method Details
#add_named_route(name, path, options = {}) ⇒ Object
272 273 274 275 276 |
# File 'lib/action_controller/routing/route_set.rb', line 272 def add_named_route(name, path, = {}) # TODO - is options EVER used? name = [:name_prefix] + name.to_s if [:name_prefix] named_routes[name.to_sym] = add_route(path, ) end |
#add_route(path, options = {}) ⇒ Object
266 267 268 269 270 |
# File 'lib/action_controller/routing/route_set.rb', line 266 def add_route(path, = {}) route = builder.build(path, ) routes << route route end |
#build_expiry(options, recall) ⇒ Object
296 297 298 299 300 301 |
# File 'lib/action_controller/routing/route_set.rb', line 296 def build_expiry(, recall) recall.inject({}) do |expiry, (key, recalled_value)| expiry[key] = (.key?(key) && [key].to_param != recalled_value.to_param) expiry end end |
#builder ⇒ Object
Subclasses and plugins may override this method to specify a different RouteBuilder instance, so that other route DSL’s can be created.
208 209 210 |
# File 'lib/action_controller/routing/route_set.rb', line 208 def builder @builder ||= RouteBuilder.new end |
#clear! ⇒ Object
218 219 220 221 222 223 224 225 226 |
# File 'lib/action_controller/routing/route_set.rb', line 218 def clear! routes.clear named_routes.clear @combined_regexp = nil @routes_by_controller = nil # This will force routing/recognition_optimization.rb # to refresh optimisations. clear_recognize_optimized! end |
#draw {|Mapper.new(self)| ... } ⇒ Object
212 213 214 215 216 |
# File 'lib/action_controller/routing/route_set.rb', line 212 def draw clear! yield Mapper.new(self) install_helpers end |
#empty? ⇒ Boolean
233 234 235 |
# File 'lib/action_controller/routing/route_set.rb', line 233 def empty? routes.empty? end |
#extra_keys(options, recall = {}) ⇒ Object
Generate the path indicated by the arguments, and return an array of the keys that were not used to generate it.
305 306 307 |
# File 'lib/action_controller/routing/route_set.rb', line 305 def extra_keys(, recall={}) generate_extras(, recall).last end |
#extract_request_environment(request) ⇒ Object
Subclasses and plugins may override this method to extract further attributes from the request, for use by route conditions and such.
436 437 438 |
# File 'lib/action_controller/routing/route_set.rb', line 436 def extract_request_environment(request) { :method => request.method } end |
#generate(options, recall = {}, method = :generate) ⇒ Object
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 |
# File 'lib/action_controller/routing/route_set.rb', line 313 def generate(, recall = {}, method=:generate) named_route_name = .delete(:use_route) generate_all = .delete(:generate_all) if named_route_name named_route = named_routes[named_route_name] = named_route.parameter_shell.merge() end = () expire_on = build_expiry(, recall) if [:controller] [:controller] = [:controller].to_s end # if the controller has changed, make sure it changes relative to the # current controller module, if any. In other words, if we're currently # on admin/get, and the new controller is 'set', the new controller # should really be admin/set. if !named_route && expire_on[:controller] && [:controller] && [:controller][0] != ?/ old_parts = recall[:controller].split('/') new_parts = [:controller].split('/') parts = old_parts[0..-(new_parts.length + 1)] + new_parts [:controller] = parts.join('/') end # drop the leading '/' on the controller name [:controller] = [:controller][1..-1] if [:controller] && [:controller][0] == ?/ merged = recall.merge() if named_route path = named_route.generate(, merged, expire_on) if path.nil? raise_named_route_error(, named_route, named_route_name) else return path end else merged[:action] ||= 'index' [:action] ||= 'index' controller = merged[:controller] action = merged[:action] raise RoutingError, "Need controller and action!" unless controller && action if generate_all # Used by caching to expire all paths for a resource return routes.collect do |route| route.__send__(method, , merged, expire_on) end.compact end # don't use the recalled keys when determining which routes to check routes = routes_by_controller[controller][action][.keys.sort_by { |x| x.object_id }] routes.each do |route| results = route.__send__(method, , merged, expire_on) return results if results && (!results.is_a?(Array) || results.first) end end raise RoutingError, "No route matches #{.inspect}" end |
#generate_code(list, padding = ' ', level = 0) ⇒ Object
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/action_controller/routing/recognition_optimisation.rb', line 89 def generate_code(list, padding=' ', level = 0) # a digit return padding + "#{list[0]}\n" if list.size == 1 && !(Array === list[0]) body = padding + "(seg = segments[#{level}]; \n" i = 0 was_nil = false list.each do |item| if Array === item i += 1 start = (i == 1) final = (i == list.size) tag, sub = item if tag == :dynamic body += padding + "#{start ? 'if' : 'elsif'} true\n" body += generate_code(sub, padding + " ", level + 1) break elsif tag == nil && !was_nil was_nil = true body += padding + "#{start ? 'if' : 'elsif'} seg.nil?\n" body += generate_code(sub, padding + " ", level + 1) else body += padding + "#{start ? 'if' : 'elsif'} seg == '#{tag}'\n" body += generate_code(sub, padding + " ", level + 1) end end end body += padding + "else\n" body += padding + " #{list[0]}\n" body += padding + "end)\n" body end |
#generate_extras(options, recall = {}) ⇒ Object
309 310 311 |
# File 'lib/action_controller/routing/route_set.rb', line 309 def generate_extras(, recall={}) generate(, recall, :generate_extras) end |
#install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) ⇒ Object
228 229 230 231 |
# File 'lib/action_controller/routing/route_set.rb', line 228 def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) Array(destinations).each { |d| d.module_eval { include Helpers } } named_routes.install(destinations, regenerate_code) end |
#load! ⇒ Object Also known as: reload!
237 238 239 240 241 |
# File 'lib/action_controller/routing/route_set.rb', line 237 def load! Routing.use_controllers! nil # Clear the controller cache so we may discover new ones clear! load_routes! end |
#load_routes! ⇒ Object
257 258 259 260 261 262 263 264 |
# File 'lib/action_controller/routing/route_set.rb', line 257 def load_routes! if configuration_file load configuration_file @routes_last_modified = File.stat(configuration_file).mtime else add_route ":controller/:action/:id" end end |
#options_as_params(options) ⇒ Object
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 |
# File 'lib/action_controller/routing/route_set.rb', line 278 def () # If an explicit :controller was given, always make :action explicit # too, so that action expiry works as expected for things like # # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'}) # # (the above is from the unit tests). In the above case, because the # controller was explicitly given, but no action, the action is implied to # be "index", not the recalled action of "show". # # great fun, eh? = .clone [:action] ||= 'index' if [:controller] [:action] = [:action].to_s if [:action] end |
#raise_named_route_error(options, named_route, named_route_name) ⇒ Object
try to give a helpful error message when named route generation fails
378 379 380 381 382 383 384 385 386 387 |
# File 'lib/action_controller/routing/route_set.rb', line 378 def raise_named_route_error(, named_route, named_route_name) diff = named_route.requirements.diff() unless diff.empty? raise RoutingError, "#{named_route_name}_url failed to generate from #{.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff().inspect}" else required_segments = named_route.segments.select {|seg| (!seg.optional?) && (!seg.is_a?(DividerSegment)) } required_keys_or_values = required_segments.map { |seg| seg.key rescue seg.value } # we want either the key or the value from the segment raise RoutingError, "#{named_route_name}_url failed to generate from #{.inspect} - you may have ambiguous routes, or you may need to supply additional parameters for this route. content_url has the following required parameters: #{required_keys_or_values.inspect} - are they all satisfied?" end end |
#recognize(request) ⇒ Object
389 390 391 392 393 |
# File 'lib/action_controller/routing/route_set.rb', line 389 def recognize(request) params = recognize_path(request.path, extract_request_environment(request)) request.path_parameters = params.with_indifferent_access "#{params[:controller].camelize}Controller".constantize end |
#recognize_path(path, environment = {}) ⇒ Object
395 396 397 |
# File 'lib/action_controller/routing/route_set.rb', line 395 def recognize_path(path, environment={}) raise "Not optimized! Check that routing/recognition_optimisation overrides RouteSet#recognize_path." end |
#reload ⇒ Object
246 247 248 249 250 251 252 253 254 255 |
# File 'lib/action_controller/routing/route_set.rb', line 246 def reload if @routes_last_modified && configuration_file mtime = File.stat(configuration_file).mtime # if it hasn't been changed, then just return return if mtime == @routes_last_modified # if it has changed then record the new time and fall to the load! below @routes_last_modified = mtime end load! end |
#routes_by_controller ⇒ Object
399 400 401 402 403 404 405 406 407 |
# File 'lib/action_controller/routing/route_set.rb', line 399 def routes_by_controller @routes_by_controller ||= Hash.new do |controller_hash, controller| controller_hash[controller] = Hash.new do |action_hash, action| action_hash[action] = Hash.new do |key_hash, keys| key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys) end end end end |
#routes_for(options, merged, expire_on) ⇒ Object
409 410 411 412 413 414 415 416 |
# File 'lib/action_controller/routing/route_set.rb', line 409 def routes_for(, merged, expire_on) raise "Need controller and action!" unless controller && action controller = merged[:controller] merged = if expire_on[:controller] action = merged[:action] || 'index' routes_by_controller[controller][action][merged.keys] end |
#routes_for_controller_and_action(controller, action) ⇒ Object
418 419 420 421 422 423 |
# File 'lib/action_controller/routing/route_set.rb', line 418 def routes_for_controller_and_action(controller, action) selected = routes.select do |route| route.matches_controller_and_action? controller, action end (selected.length == routes.length) ? routes : selected end |
#routes_for_controller_and_action_and_keys(controller, action, keys) ⇒ Object
425 426 427 428 429 430 431 432 |
# File 'lib/action_controller/routing/route_set.rb', line 425 def routes_for_controller_and_action_and_keys(controller, action, keys) selected = routes.select do |route| route.matches_controller_and_action? controller, action end selected.sort_by do |route| (keys - route.significant_keys).length end end |
#segment_tree(routes) ⇒ Object
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/action_controller/routing/recognition_optimisation.rb', line 70 def segment_tree(routes) tree = [0] i = -1 routes.each do |route| i += 1 # not fast, but runs only once segments = to_plain_segments(route.segments.inject("") { |str,s| str << s.to_s }) node = tree segments.each do |seg| seg = :dynamic if seg && seg[0] == ?: node << [seg, [i]] if node.empty? || node[node.size - 1][0] != seg node = node[node.size - 1][1] end end tree end |
#to_plain_segments(str) ⇒ Object
this must be really fast
124 125 126 127 128 129 130 131 |
# File 'lib/action_controller/routing/recognition_optimisation.rb', line 124 def to_plain_segments(str) str = str.dup str.sub!(/^\/+/,'') str.sub!(/\/+$/,'') segments = str.split(/\.[^\/]+\/+|\/+|\.[^\/]+\Z/) # cut off ".format" also segments << nil segments end |