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.
211 212 213 214 215 216 217 218 |
# File 'lib/action_controller/routing/route_set.rb', line 211 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.
209 210 211 |
# File 'lib/action_controller/routing/route_set.rb', line 209 def configuration_files @configuration_files end |
#named_routes ⇒ Object
Returns the value of attribute named_routes.
209 210 211 |
# File 'lib/action_controller/routing/route_set.rb', line 209 def named_routes @named_routes end |
#routes ⇒ Object
Returns the value of attribute routes.
209 210 211 |
# File 'lib/action_controller/routing/route_set.rb', line 209 def routes @routes end |
Instance Method Details
#add_configuration_file(path) ⇒ Object
250 251 252 |
# File 'lib/action_controller/routing/route_set.rb', line 250 def add_configuration_file(path) self.configuration_files << path end |
#add_named_route(name, path, options = {}) ⇒ Object
315 316 317 318 319 |
# File 'lib/action_controller/routing/route_set.rb', line 315 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
308 309 310 311 312 313 |
# File 'lib/action_controller/routing/route_set.rb', line 308 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
339 340 341 342 343 344 |
# File 'lib/action_controller/routing/route_set.rb', line 339 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.
222 223 224 |
# File 'lib/action_controller/routing/route_set.rb', line 222 def builder @builder ||= RouteBuilder.new end |
#call(env) ⇒ Object
435 436 437 438 439 |
# File 'lib/action_controller/routing/route_set.rb', line 435 def call(env) request = Request.new(env) app = Routing::Routes.recognize(request) app.call(env).to_a end |
#clear! ⇒ Object
231 232 233 234 235 236 237 238 239 |
# File 'lib/action_controller/routing/route_set.rb', line 231 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
260 261 262 |
# File 'lib/action_controller/routing/route_set.rb', line 260 def configuration_file configuration_files end |
#configuration_file=(path) ⇒ Object
Deprecated accessor
255 256 257 |
# File 'lib/action_controller/routing/route_set.rb', line 255 def configuration_file=(path) add_configuration_file(path) end |
#deprecated_routes_for_controller_and_action_and_keys(controller, action, keys) ⇒ Object
487 488 489 490 491 492 493 494 |
# File 'lib/action_controller/routing/route_set.rb', line 487 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
226 227 228 229 |
# File 'lib/action_controller/routing/route_set.rb', line 226 def draw yield Mapper.new(self) install_helpers end |
#empty? ⇒ Boolean
246 247 248 |
# File 'lib/action_controller/routing/route_set.rb', line 246 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.
348 349 350 |
# File 'lib/action_controller/routing/route_set.rb', line 348 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.
498 499 500 |
# File 'lib/action_controller/routing/route_set.rb', line 498 def extract_request_environment(request) { :method => request.method } end |
#generate(options, recall = {}, method = :generate) ⇒ Object
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 421 |
# File 'lib/action_controller/routing/route_set.rb', line 356 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
352 353 354 |
# File 'lib/action_controller/routing/route_set.rb', line 352 def generate_extras(, recall={}) generate(, recall, :generate_extras) end |
#install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) ⇒ Object
241 242 243 244 |
# File 'lib/action_controller/routing/route_set.rb', line 241 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!
264 265 266 267 268 |
# File 'lib/action_controller/routing/route_set.rb', line 264 def load! Routing.use_controllers!(nil) # Clear the controller cache so we may discover new ones clear! load_routes! end |
#load_routes! ⇒ Object
285 286 287 288 289 290 291 292 |
# File 'lib/action_controller/routing/route_set.rb', line 285 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
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 |
# File 'lib/action_controller/routing/route_set.rb', line 321 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
424 425 426 427 428 429 430 431 432 433 |
# File 'lib/action_controller/routing/route_set.rb', line 424 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
441 442 443 444 445 |
# File 'lib/action_controller/routing/route_set.rb', line 441 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
447 448 449 |
# File 'lib/action_controller/routing/route_set.rb', line 447 def recognize_path(path, environment={}) raise "Not optimized! Check that routing/recognition_optimisation overrides RouteSet#recognize_path." end |
#reload ⇒ Object
273 274 275 276 277 278 279 280 281 282 283 |
# File 'lib/action_controller/routing/route_set.rb', line 273 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
451 452 453 454 455 456 457 458 459 460 461 462 |
# File 'lib/action_controller/routing/route_set.rb', line 451 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
294 295 296 297 298 299 300 301 302 303 304 305 306 |
# File 'lib/action_controller/routing/route_set.rb', line 294 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
464 465 466 467 468 469 470 471 |
# File 'lib/action_controller/routing/route_set.rb', line 464 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
473 474 475 476 477 478 479 |
# File 'lib/action_controller/routing/route_set.rb', line 473 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
481 482 483 484 485 |
# File 'lib/action_controller/routing/route_set.rb', line 481 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 |