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_files ⇒ Object
Returns the value of attribute configuration_files.
-
#named_routes ⇒ Object
Returns the value of attribute named_routes.
-
#routes ⇒ Object
Returns the value of attribute routes.
Instance Method Summary collapse
- #add_configuration_file(path) ⇒ Object
- #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.
- #call(env) ⇒ Object
- #clear! ⇒ Object
-
#configuration_file ⇒ Object
Deprecated accessor.
-
#configuration_file=(path) ⇒ Object
Deprecated accessor.
- #deprecated_routes_for_controller_and_action_and_keys(controller, action, keys) ⇒ 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_changed_at ⇒ 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.
210 211 212 213 214 215 216 217 |
# File 'lib/action_controller/routing/route_set.rb', line 210 def initialize self.configuration_files = [] self.routes = [] self.named_routes = NamedRouteCollection.new clear_recognize_optimized! end |
Instance Attribute Details
#configuration_files ⇒ Object
Returns the value of attribute configuration_files.
208 209 210 |
# File 'lib/action_controller/routing/route_set.rb', line 208 def configuration_files @configuration_files end |
#named_routes ⇒ Object
Returns the value of attribute named_routes.
208 209 210 |
# File 'lib/action_controller/routing/route_set.rb', line 208 def named_routes @named_routes end |
#routes ⇒ Object
Returns the value of attribute routes.
208 209 210 |
# File 'lib/action_controller/routing/route_set.rb', line 208 def routes @routes end |
Instance Method Details
#add_configuration_file(path) ⇒ Object
249 250 251 |
# File 'lib/action_controller/routing/route_set.rb', line 249 def add_configuration_file(path) self.configuration_files << path end |
#add_named_route(name, path, options = {}) ⇒ Object
314 315 316 317 318 |
# File 'lib/action_controller/routing/route_set.rb', line 314 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
307 308 309 310 311 312 |
# File 'lib/action_controller/routing/route_set.rb', line 307 def add_route(path, = {}) .each { |k, v| [k] = v.to_s if [:controller, :action].include?(k) && v.is_a?(Symbol) } route = builder.build(path, ) routes << route route end |
#build_expiry(options, recall) ⇒ Object
338 339 340 341 342 343 |
# File 'lib/action_controller/routing/route_set.rb', line 338 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.
221 222 223 |
# File 'lib/action_controller/routing/route_set.rb', line 221 def builder @builder ||= RouteBuilder.new end |
#call(env) ⇒ Object
434 435 436 437 438 |
# File 'lib/action_controller/routing/route_set.rb', line 434 def call(env) request = Request.new(env) app = Routing::Routes.recognize(request) app.call(env).to_a end |
#clear! ⇒ Object
230 231 232 233 234 235 236 237 238 |
# File 'lib/action_controller/routing/route_set.rb', line 230 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 |
#configuration_file ⇒ Object
Deprecated accessor
259 260 261 |
# File 'lib/action_controller/routing/route_set.rb', line 259 def configuration_file configuration_files end |
#configuration_file=(path) ⇒ Object
Deprecated accessor
254 255 256 |
# File 'lib/action_controller/routing/route_set.rb', line 254 def configuration_file=(path) add_configuration_file(path) end |
#deprecated_routes_for_controller_and_action_and_keys(controller, action, keys) ⇒ Object
486 487 488 489 490 491 492 493 |
# File 'lib/action_controller/routing/route_set.rb', line 486 def deprecated_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 |
#draw {|Mapper.new(self)| ... } ⇒ Object
225 226 227 228 |
# File 'lib/action_controller/routing/route_set.rb', line 225 def draw yield Mapper.new(self) install_helpers end |
#empty? ⇒ Boolean
245 246 247 |
# File 'lib/action_controller/routing/route_set.rb', line 245 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.
347 348 349 |
# File 'lib/action_controller/routing/route_set.rb', line 347 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.
497 498 499 |
# File 'lib/action_controller/routing/route_set.rb', line 497 def extract_request_environment(request) { :method => request.method } end |
#generate(options, recall = {}, method = :generate) ⇒ Object
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 |
# File 'lib/action_controller/routing/route_set.rb', line 355 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 future_routes, deprecated_routes = routes_by_controller[controller][action][.reject {|k,v| !v}.keys.sort_by { |x| x.object_id }] routes = Routing.generate_best_match ? deprecated_routes : future_routes routes.each_with_index do |route, index| results = route.__send__(method, , merged, expire_on) if results && (!results.is_a?(Array) || results.first) return results end 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 |
# 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) 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
351 352 353 |
# File 'lib/action_controller/routing/route_set.rb', line 351 def generate_extras(, recall={}) generate(, recall, :generate_extras) end |
#install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) ⇒ Object
240 241 242 243 |
# File 'lib/action_controller/routing/route_set.rb', line 240 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!
263 264 265 266 267 |
# File 'lib/action_controller/routing/route_set.rb', line 263 def load! Routing.use_controllers!(nil) # Clear the controller cache so we may discover new ones clear! load_routes! end |
#load_routes! ⇒ Object
284 285 286 287 288 289 290 291 |
# File 'lib/action_controller/routing/route_set.rb', line 284 def load_routes! if configuration_files.any? configuration_files.each { |config| load(config) } @routes_last_modified = routes_changed_at else add_route ":controller/:action/:id" end end |
#options_as_params(options) ⇒ Object
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/action_controller/routing/route_set.rb', line 320 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
423 424 425 426 427 428 429 430 431 432 |
# File 'lib/action_controller/routing/route_set.rb', line 423 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
440 441 442 443 444 |
# File 'lib/action_controller/routing/route_set.rb', line 440 def recognize(request) params = recognize_path(request.path, extract_request_environment(request)) request.path_parameters = params.with_indifferent_access "#{params[:controller].to_s.camelize}Controller".constantize end |
#recognize_path(path, environment = {}) ⇒ Object
446 447 448 |
# File 'lib/action_controller/routing/route_set.rb', line 446 def recognize_path(path, environment={}) raise "Not optimized! Check that routing/recognition_optimisation overrides RouteSet#recognize_path." end |
#reload ⇒ Object
272 273 274 275 276 277 278 279 280 281 282 |
# File 'lib/action_controller/routing/route_set.rb', line 272 def reload if configuration_files.any? && @routes_last_modified if routes_changed_at == @routes_last_modified return # routes didn't change, don't reload else @routes_last_modified = routes_changed_at end end load! end |
#routes_by_controller ⇒ Object
450 451 452 453 454 455 456 457 458 459 460 461 |
# File 'lib/action_controller/routing/route_set.rb', line 450 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), deprecated_routes_for_controller_and_action_and_keys(controller, action, keys) ] end end end end |
#routes_changed_at ⇒ Object
293 294 295 296 297 298 299 300 301 302 303 304 305 |
# File 'lib/action_controller/routing/route_set.rb', line 293 def routes_changed_at routes_changed_at = nil configuration_files.each do |config| config_changed_at = File.stat(config).mtime if routes_changed_at.nil? || config_changed_at > routes_changed_at routes_changed_at = config_changed_at end end routes_changed_at end |
#routes_for(options, merged, expire_on) ⇒ Object
463 464 465 466 467 468 469 470 |
# File 'lib/action_controller/routing/route_set.rb', line 463 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][1] end |
#routes_for_controller_and_action(controller, action) ⇒ Object
472 473 474 475 476 477 478 |
# File 'lib/action_controller/routing/route_set.rb', line 472 def routes_for_controller_and_action(controller, action) ActiveSupport::Deprecation.warn "routes_for_controller_and_action() has been deprecated. Please use routes_for()" 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
480 481 482 483 484 |
# File 'lib/action_controller/routing/route_set.rb', line 480 def routes_for_controller_and_action_and_keys(controller, action, keys) routes.select do |route| route.matches_controller_and_action? controller, action 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
123 124 125 126 127 128 129 130 |
# File 'lib/action_controller/routing/recognition_optimisation.rb', line 123 def to_plain_segments(str) str = str.dup str.sub!(/^\/+/,'') str.sub!(/\/+$/,'') segments = str.split(/\.[^\/]+\/+|\/+|\.[^\/]+\Z/) # cut off ".format" also segments << nil segments end |