Changeset 8082
- Timestamp:
- 11/06/07 08:09:39 (8 months ago)
- Files:
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/actionpack/lib/action_controller/routing.rb
r7849 r8082 32 32 Regexp.new("|#{source}").match('').captures.length 33 33 end 34 34 35 35 class << self 36 36 def optionalize(pattern) … … 40 40 end 41 41 end 42 42 43 43 def unoptionalize(pattern) 44 44 [/\A\(\?:(.*)\)\?\Z/, /\A(.|\(.*\))\?\Z/].each do |regexp| … … 51 51 52 52 module ActionController 53 # == Routing 53 # == Routing 54 54 # 55 55 # The routing module provides URL rewriting in native Ruby. It's a way to 56 56 # redirect incoming requests to controllers and actions. This replaces 57 # mod_rewrite rules. Best of all Rails' Routing works with any web server.57 # mod_rewrite rules. Best of all, Rails' Routing works with any web server. 58 58 # Routes are defined in routes.rb in your RAILS_ROOT/config directory. 59 59 # 60 # Consider the following route, installed by Rails when you generate your 60 # Consider the following route, installed by Rails when you generate your 61 61 # application: 62 62 # 63 63 # map.connect ':controller/:action/:id' 64 64 # 65 # This route states that it expects requests to consist of a 66 # :controller followed by an :action that in turn s is fed by some :id67 # 68 # Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end up 65 # This route states that it expects requests to consist of a 66 # :controller followed by an :action that in turn is fed some :id. 67 # 68 # Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end up 69 69 # with: 70 70 # 71 71 # params = { :controller => 'blog', 72 # :action => 'edit' 72 # :action => 'edit', 73 73 # :id => '22' 74 74 # } 75 75 # 76 # Think of creating routes as drawing a map for your requests. The map tells 76 # Think of creating routes as drawing a map for your requests. The map tells 77 77 # them where to go based on some predefined pattern: 78 78 # … … 87 87 # :controller maps to your controller name 88 88 # :action maps to an action with your controllers 89 # 89 # 90 90 # Other names simply map to a parameter as in the case of +:id+. 91 # 91 # 92 92 # == Route priority 93 93 # 94 # Not all routes are created equally. Routes have priority defined by the 94 # Not all routes are created equally. Routes have priority defined by the 95 95 # order of appearance of the routes in the routes.rb file. The priority goes 96 96 # from top to bottom. The last route in that file is at the lowest priority 97 # will be applied last. If no route matches, 404 is returned.98 # 99 # Within blocks, the empty pattern goes first i.e.is at the highest priority.97 # and will be applied last. If no route matches, 404 is returned. 98 # 99 # Within blocks, the empty pattern is at the highest priority. 100 100 # In practice this works out nicely: 101 101 # 102 # ActionController::Routing::Routes.draw do |map| 102 # ActionController::Routing::Routes.draw do |map| 103 103 # map.with_options :controller => 'blog' do |blog| 104 # blog.show '', :action => 'list'104 # blog.show '', :action => 'list' 105 105 # end 106 # map.connect ':controller/:action/:view 106 # map.connect ':controller/:action/:view' 107 107 # end 108 108 # 109 # In this case, invoking blog controller (with an URL like '/blog/') 109 # In this case, invoking blog controller (with an URL like '/blog/') 110 110 # without parameters will activate the 'list' action by default. 111 111 # 112 112 # == Defaults routes and default parameters 113 113 # 114 # Setting a default route is straightforward in Rails because by appendinga115 # Hash to the end of your mapping you can setdefault parameters.114 # Setting a default route is straightforward in Rails - you simply append a 115 # Hash at the end of your mapping to set any default parameters. 116 116 # 117 117 # Example: … … 120 120 # end 121 121 # 122 # This sets up +blog+ as the default controller if no other is specified.122 # This sets up +blog+ as the default controller if no other is specified. 123 123 # This means visiting '/' would invoke the blog controller. 124 124 # 125 125 # More formally, you can define defaults in a route with the +:defaults+ key. 126 # 127 # map.connect ':controller/: id/:action', :action => 'show', :defaults => { :page => 'Dashboard' }126 # 127 # map.connect ':controller/:action/:id', :action => 'show', :defaults => { :page => 'Dashboard' } 128 128 # 129 129 # == Named routes … … 144 144 # redirect_to show_item_path(:id => 25) 145 145 # 146 # Use <tt>map.root</tt> as a shorthand to name a route for the root path "" 146 # Use <tt>map.root</tt> as a shorthand to name a route for the root path "". 147 147 # 148 148 # # In routes.rb … … 167 167 # 168 168 # # provides named routes for show, delete, and edit 169 # link_to @article.title, show_path(:id => @article.id) 169 # link_to @article.title, show_path(:id => @article.id) 170 170 # 171 171 # == Pretty URLs … … 174 174 # 175 175 # map.connect 'articles/:year/:month/:day', 176 # :controller => 'articles', 176 # :controller => 'articles', 177 177 # :action => 'find_by_date', 178 178 # :year => /\d{4}/, 179 # :month => /\d{1,2}/,180 # :day => /\d{1,2}/181 # 179 # :month => /\d{1,2}/, 180 # :day => /\d{1,2}/ 181 # 182 182 # # Using the route above, the url below maps to: 183 183 # # params = {:year => '2005', :month => '11', :day => '06'} … … 185 185 # 186 186 # == Regular Expressions and parameters 187 # You can specify a re qular expression to define a format for a parameter.187 # You can specify a regular expression to define a format for a parameter. 188 188 # 189 189 # map.geocode 'geocode/:postalcode', :controller => 'geocode', … … 192 192 # or, more formally: 193 193 # 194 # map.geocode 'geocode/:postalcode', :controller => 'geocode', 194 # map.geocode 'geocode/:postalcode', :controller => 'geocode', 195 195 # :action => 'show', :requirements => { :postalcode => /\d{5}(-\d{4})?/ } 196 196 # … … 219 219 # 220 220 # === +assert_routing+ 221 # 221 # 222 222 # def test_movie_route_properly_splits 223 223 # opts = {:controller => "plugin", :action => "checkout", :id => "2"} 224 224 # assert_routing "plugin/checkout/2", opts 225 225 # end 226 # 226 # 227 227 # +assert_routing+ lets you test whether or not the route properly resolves into options. 228 228 # … … 231 231 # def test_route_has_options 232 232 # opts = {:controller => "plugin", :action => "show", :id => "12"} 233 # assert_recognizes opts, "/plugins/show/12" 233 # assert_recognizes opts, "/plugins/show/12" 234 234 # end 235 # 235 # 236 236 # Note the subtle difference between the two: +assert_routing+ tests that 237 # a n URL fits options while +assert_recognizes+ tests that anURL237 # a URL fits options while +assert_recognizes+ tests that a URL 238 238 # breaks into parameters properly. 239 239 # … … 259 259 mattr_accessor :controller_paths 260 260 self.controller_paths = [] 261 261 262 262 # A helper module to hold URL related helpers. 263 263 module Helpers 264 264 include PolymorphicRoutes 265 265 end 266 266 267 267 class << self 268 268 def with_controllers(names) … … 295 295 unless @possible_controllers 296 296 @possible_controllers = [] 297 297 298 298 paths = controller_paths.select { |path| File.directory?(path) && path != "." } 299 299 … … 302 302 Dir["#{load_path}/**/*_controller.rb"].collect do |path| 303 303 next if seen_paths[path.gsub(%r{^\.[/\\]}, "")] 304 304 305 305 controller_name = path[(load_path.length + 1)..-1] 306 306 307 307 controller_name.gsub!(/_controller\.rb\Z/, '') 308 308 @possible_controllers << controller_name … … 325 325 elsif %r{^(.*)/} =~ previous then "#{$1}/#{controller}" 326 326 else controller 327 end 328 end 327 end 328 end 329 329 end 330 330 331 331 class Route #:nodoc: 332 332 attr_accessor :segments, :requirements, :conditions, :optimise 333 333 334 334 def initialize 335 335 @segments = [] … … 337 337 @conditions = {} 338 338 end 339 339 340 340 # Indicates whether the routes should be optimised with the string interpolation 341 341 # version of the named routes methods. … … 343 343 @optimise && ActionController::Base::optimise_named_routes 344 344 end 345 345 346 346 def segment_keys 347 347 segments.collect do |segment| … … 349 349 end.compact 350 350 end 351 351 352 352 # Write and compile a +generate+ method for this Route. 353 353 def write_generation 354 354 # Build the main body of the generation 355 355 body = "expired = false\n#{generation_extraction}\n#{generation_structure}" 356 356 357 357 # If we have conditions that must be tested first, nest the body inside an if 358 358 body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements … … 376 376 raw_method 377 377 end 378 378 379 379 # Build several lines of code that extract values from the options hash. If any 380 380 # of the values are missing or rejected then a return will be executed. … … 384 384 end.compact * "\n" 385 385 end 386 386 387 387 # Produce a condition expression that will check the requirements of this route 388 388 # upon generation. … … 398 398 requirement_conditions * ' && ' unless requirement_conditions.empty? 399 399 end 400 400 401 401 def generation_structure 402 402 segments.last.string_structure segments[0..-2] 403 403 end 404 404 405 405 # Write and compile a +recognize+ method for this Route. 406 406 def write_recognition … … 408 408 body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams" 409 409 body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend" 410 410 411 411 # Build the method declaration and compile it 412 412 method_decl = "def recognize(path, env={})\n#{body}\nend" … … 432 432 wrap ? ("\\A" + pattern + "\\Z") : pattern 433 433 end 434 434 435 435 # Write the code to extract the parameters from a matched route. 436 436 def recognition_extraction … … 443 443 extraction.compact 444 444 end 445 445 446 446 # Write the real generation implementation and then resend the message. 447 447 def generate(options, hash, expire_on = {}) … … 494 494 recognize path, environment 495 495 end 496 496 497 497 # A route's parameter shell contains parameter values that are not in the 498 498 # route's path, but should be placed in the recognized hash. 499 # 499 # 500 500 # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route: 501 # 501 # 502 502 # map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/ 503 # 503 # 504 504 def parameter_shell 505 505 @parameter_shell ||= returning({}) do |shell| … … 509 509 end 510 510 end 511 511 512 512 # Return an array containing all the keys that are used in this route. This 513 513 # includes keys that appear inside the path, and keys that have requirements … … 553 553 end 554 554 end 555 555 556 556 protected 557 557 def requirement_for(key) … … 600 600 "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}" 601 601 end 602 602 603 603 def string_structure(prior_segments) 604 604 optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments) 605 605 end 606 606 607 607 # Return an if condition that is true if all the prior segments can be generated. 608 608 # If there are no optional segments before this one, then nil is returned. … … 611 611 optional_locals.empty? ? nil : " if #{optional_locals * ' && '}" 612 612 end 613 613 614 614 # Recognition 615 615 616 616 def match_extraction(next_capture) 617 617 nil 618 618 end 619 619 620 620 # Warning 621 621 622 622 # Returns true if this segment is optional? because of a default. If so, then 623 623 # no warning will be emitted regarding this segment. … … 630 630 attr_accessor :value, :raw 631 631 alias_method :raw?, :raw 632 632 633 633 def initialize(value = nil) 634 634 super() 635 635 self.value = value 636 636 end 637 637 638 638 def interpolation_chunk 639 639 raw? ? value : super 640 640 end 641 641 642 642 def regexp_chunk 643 643 chunk = Regexp.escape(value) 644 644 optional? ? Regexp.optionalize(chunk) : chunk 645 645 end 646 646 647 647 def build_pattern(pattern) 648 648 escaped = Regexp.escape(value) … … 655 655 end 656 656 end 657 657 658 658 def to_s 659 659 value … … 667 667 self.is_optional = true 668 668 end 669 669 670 670 def optionality_implied? 671 671 true … … 675 675 class DynamicSegment < Segment #:nodoc: 676 676 attr_accessor :key, :default, :regexp 677 677 678 678 def initialize(key = nil, options = {}) 679 679 super() … … 682 682 self.is_optional = true if options[:optional] || options.key?(:default) 683 683 end 684 684 685 685 def to_s 686 686 ":#{key}" 687 687 end 688 688 689 689 # The local variable name that the value of this segment will be extracted to. 690 690 def local_name 691 691 "#{key}_value" 692 692 end 693 693 694 694 def extract_value 695 695 "#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}" … … 709 709 "expired, hash = true, options if !expired && expire_on[:#{key}]" 710 710 end 711 711 712 712 def extraction_code 713 713 s = extract_value … … 716 716 s << "\n#{expiry_statement}" 717 717 end 718 718 719 719 def interpolation_chunk(value_code = "#{local_name}") 720 720 "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}" 721 721 end 722 722 723 723 def string_structure(prior_segments) 724 724 if optional? # We have a conditional to do... … … 726 726 # segments. This occurs if our value is the default value, or, if we are 727 727 # optional, if we have nil as our value. 728 "if #{local_name} == #{default.inspect}\n" + 729 continue_string_structure(prior_segments) + 728 "if #{local_name} == #{default.inspect}\n" + 729 continue_string_structure(prior_segments) + 730 730 "\nelse\n" + # Otherwise, write the code up to here 731 731 "#{interpolation_statement(prior_segments)}\nend" … … 734 734 end 735 735 end 736 736 737 737 def value_regexp 738 738 Regexp.new "\\A#{regexp.source}\\Z" if regexp … … 741 741 regexp ? "(#{regexp.source})" : "([^#{Routing::SEPARATORS.join}]+)" 742 742 end 743 743 744 744 def build_pattern(pattern) 745 745 chunk = regexp_chunk … … 761 761 ] 762 762 end 763 763 764 764 def optionality_implied? 765 765 [:action, :id].include? key 766 766 end 767 767 768 768 end 769 769 … … 823 823 824 824 class Result < ::Array #:nodoc: 825 def to_s() join '/' end 825 def to_s() join '/' end 826 826 def self.new_escaped(strings) 827 827 new strings.collect {|str| URI.unescape str} 828 end 828 end 829 829 end 830 830 end … … 832 832 class RouteBuilder #:nodoc: 833 833 attr_accessor :separators, :optional_separators 834 834 835 835 def initialize 836 836 self.separators = Routing::SEPARATORS 837 837 self.optional_separators = %w( / ) 838 838 end 839 839 840 840 def separator_pattern(inverted = false) 841 841 "[#{'^' if inverted}#{Regexp.escape(separators.join)}]" 842 842 end 843 843 844 844 def interval_regexp 845 845 Regexp.new "(.*?)(#{separators.source}|$)" 846 846 end 847 847 848 848 # Accepts a "route path" (a string defining a route), and returns the array 849 849 # of segments that corresponds to it. Note that the segment array is only … … 854 854 def segments_for_route_path(path) 855 855 rest, segments = path, [] 856 856 857 857 until rest.empty? 858 858 segment, rest = segment_for rest … … 885 885 [segment, $~.post_match] 886 886 end 887 887 888 888 # Split the given hash of options into requirement and default hashes. The 889 889 # segments are passed alongside in order to distinguish between default values … … 891 891 def divide_route_options(segments, options) 892 892 options = options.dup 893 893 894 894 if options[:namespace] 895 895 options[:controller] = "#{options[:path_prefix]}/#{options[:controller]}" … … 897 897 options.delete(:name_prefix) 898 898 options.delete(:namespace) 899 end 900 899 end 900 901 901 requirements = (options.delete(:requirements) || {}).dup 902 902 defaults = (options.delete(:defaults) || {}).dup … … 908 908 hash[key] = value 909 909 end 910 910 911 911 [defaults, requirements, conditions] 912 912 end 913 913 914 914 # Takes a hash of defaults and a hash of requirements, and assigns them to 915 915 # the segments. Any unused requirements (which do not correspond to a segment) … … 917 917 def assign_route_options(segments, defaults, requirements) 918 918 route_requirements = {} # Requirements that do not belong to a segment 919 919 920 920 segment_named = Proc.new do |key| 921 921 segments.detect { |segment| segment.key == key if segment.respond_to?(:key) } 922 922 end 923 923 924 924 requirements.each do |key, requirement| 925 925 segment = segment_named[key] … … 934 934 end 935 935 end 936 936 937 937 defaults.each do |key, default| 938 938 segment = segment_named[key] … … 941 941 segment.default = default.to_param if default 942 942 end 943 943 944 944 assign_default_route_options(segments) 945 945 ensure_required_segments(segments) 946 946 route_requirements 947 947 end 948 948 949 949 # Assign default options, such as 'index' as a default for :action. This 950 950 # method must be run *after* user supplied requirements and defaults have … … 966 966 end 967 967 end 968 968 969 969 # Makes sure that there are no optional segments that precede a required 970 970 # segment. If any are found that precede a required segment, they are … … 985 985 end 986 986 end 987 987 988 988 # Construct and return a route with the given path and options. 989 989 def build(path, options) 990 990 # Wrap the path with slashes 991 991 path = "/#{path}" unless path[0] == ?/ 992 path = "#{path}/" unless path[-1] == ?/ 993 992 path = "#{path}/" unless path[-1] == ?/ 993 994 994 path = "/#{options[:path_prefix]}#{path}" if options[:path_prefix] 995 995 996 996 segments = segments_for_route_path(path) 997 997 defaults, requirements, conditions = divide_route_options(segments, options) … … 1003 1003 route.requirements = requirements 1004 1004 route.conditions = conditions 1005 1005 1006 1006 # Routes cannot use the current string interpolation method 1007 1007 # if there are user-supplied :requirements as the interpolation 1008 1008 # code won't raise RoutingErrors when generating 1009 route.optimise = !options.key?(:requirements) 1009 route.optimise = !options.key?(:requirements) 1010 1010 if !route.significant_keys.include?(:action) && !route.requirements[:action] 1011 1011 route.requirements[:action] = "index" … … 1024 1024 # Mapper instances are used to build routes. The object passed to the draw 1025 1025 # block in config/routes.rb is a Mapper instance. 1026 # 1026 # 1027 1027 # Mapper instances have relatively few instance methods, in order to avoid 1028 1028 # clashes with named routes. … … 1031 1031 @set = set 1032 1032 end 1033 1034 # Create an unnamed route with the provided +path+ and +options+. See 1033 1034 # Create an unnamed route with the provided +path+ and +options+. See 1035 1035 # SomeHelpfulUrl for an introduction to routes. 1036 1036 def connect(path, options = {}) … … 1046 1046 @set.add_named_route(name, path, options) 1047 1047 end 1048 1048 1049 1049 # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model. 1050 1050 # Example: … … 1065 1065 end 1066 1066 end 1067 1067 1068 1068 1069 1069 def method_missing(route_name, *args, &proc) … … 1088 1088 @routes = {} 1089 1089 @helpers = [] 1090 1090 1091 1091 @module ||= Module.new 1092 1092 @module.instance_methods.each do |selector| … … 1152 1152 end 1153 1153 end 1154 1154 1155 1155 def define_hash_access(route, name, kind, options) 1156 1156 selector = hash_access_name(name, kind) … … 1207 1207 1208 1208 attr_accessor :routes, :named_routes 1209 1209 1210 1210 def initialize 1211 1211 self.routes = [] 1212 1212 self.named_routes = NamedRouteCollection.new 1213 1213 end 1214 1214 1215 1215 # Subclasses and plugins may override this method to specify a different 1216 1216 # RouteBuilder instance, so that other route DSL's can be created. … … 1224 1224 install_helpers 1225 1225 end 1226 1226 1227 1227 def clear! 1228 1228 routes.clear … … 1231 1231 @routes_by_controller = nil 1232 1232 end 1233 1233 1234 1234 def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) 1235 1235 Array(destinations).each { |d| d.module_eval { include Helpers } } … … 1240 1240 routes.empty? 1241 1241 end 1242 1242 1243 1243 def load! 1244 1244 Routing.use_controllers! nil # Clear the controller cache so we may discover new ones … … 1250 1250 # reload! will always force a reload whereas load checks the timestamp first 1251 1251 alias reload! load! 1252 1252 1253 1253 def reload 1254 1254 if @routes_last_modified && defined?(RAILS_ROOT) … … 1261 1261 load! 1262 1262 end 1263 1263 1264 1264 def load_routes! 1265 1265 if defined?(RAILS_ROOT) && defined?(::ActionController::Routing::Routes) && self == ::ActionController::Routing::Routes … … 1270 1270 end 1271 1271 end 1272 1272 1273 1273 def add_route(path, options = {}) 1274 1274 route = builder.build(path, options) … … 1276 1276 route 1277 1277 end 1278 1278 1279 1279 def add_named_route(name, path, options = {}) 1280 1280 # TODO - is options EVER used? … … 1282 1282 named_routes[name.to_sym] = add_route(path, options) 1283 1283 end 1284 1284 1285 1285 def options_as_params(options) 1286 1286 # If an explicit :controller was given, always make :action explicit … … 1300 1300 options_as_params 1301 1301 end 1302 1302 1303 1303 def build_expiry(options, recall) 1304 1304 recall.inject({}) do |expiry, (key, recalled_value)| … … 1349 1349 if named_route 1350 1350 path = named_route.generate(options, merged, expire_on) 1351 if path.nil? 1351 if path.nil? 1352 1352 raise_named_route_error(options, named_route, named_route_name) 1353 1353 else … … 1357 1357 merged[:action] ||= 'index' 1358 1358 options[:action] ||= 'index' 1359 1359 1360 1360 controller = merged[:controller] 1361 1361 action = merged[:action] 1362 1362 1363 1363 raise RoutingError, "Need controller and action!" unless controller && action 1364 1364 1365 1365 if generate_all 1366 1366 # Used by caching to expire all paths for a resource … … 1369 1369 end.compact 1370 1370 end 1371 1371 1372 1372 # don't use the recalled keys when determining which routes to check 1373 1373 routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }] … … 1378 1378 end 1379 1379 end 1380 1380 1381 1381 raise RoutingError, "No route matches #{options.inspect}" 1382 1382 end 1383 1383 1384 1384 # try to give a helpful error message when named route generation fails 1385 1385 def raise_named_route_error(options, named_route, named_route_name) … … 1393 1393 end 1394 1394 end 1395 1395 1396 1396 def recognize(request) 1397 1397 params = recognize_path(request.path, extract_request_environment(request)) … … 1399 1399 "#{params[:controller].camelize}Controller".constantize 1400 1400 end 1401 1401 1402 1402 def recognize_path(path, environment={}) 1403 1403 routes.each do |route| … … 1415 1415 end 1416 1416 end 1417 1417 1418 1418 def routes_by_controller 1419 1419 @routes_by_controller ||= Hash.new do |controller_hash, controller| … … 1425 1425 end 1426 1426 end 1427 1427 1428 1428 def routes_for(options, merged, expire_on) 1429 1429 raise "Need controller and action!" unless controller && action … … 1431 1431 merged = options if expire_on[:controller] 1432 1432 action = merged[:action] || 'index' 1433 1433 1434 1434 routes_by_controller[controller][action][merged.keys] 1435 1435 end 1436 1436 1437 1437 def routes_for_controller_and_action(controller, action) 1438 1438 selected = routes.select do |route| … … 1441 1441 (selected.length == routes.length) ? routes : selected 1442 1442 end 1443 1443 1444 1444 def routes_for_controller_and_action_and_keys(controller, action, keys) 1445 1445 selected = routes.select do |route|