diff options
Diffstat (limited to 'actionpack/lib/action_dispatch')
-rw-r--r-- | actionpack/lib/action_dispatch/http/mime_type.rb | 1 | ||||
-rwxr-xr-x | actionpack/lib/action_dispatch/http/request.rb | 29 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/response.rb | 2 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing.rb | 383 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/deprecated_mapper.rb | 878 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/mapper.rb | 315 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/route.rb | 49 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/route_set.rb | 492 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/testing/integration.rb | 2 |
9 files changed, 2134 insertions, 17 deletions
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index e85823d8db..c30897b32a 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -24,6 +24,7 @@ module Mime LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } def self.[](type) + return type if type.is_a?(Type) Type.lookup_by_extension(type.to_s) end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index bff030f0e4..75be2cc260 100755 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -97,6 +97,10 @@ module ActionDispatch end end + def forgery_whitelisted? + method == :get || xhr? || content_type.nil? || !content_type.verify_request? + end + def media_type content_type.to_s end @@ -136,19 +140,16 @@ module ActionDispatch # If-Modified-Since and If-None-Match conditions. If both headers are # supplied, both must match, or the request is not considered fresh. def fresh?(response) - case - when if_modified_since && if_none_match - not_modified?(response.last_modified) && etag_matches?(response.etag) - when if_modified_since - not_modified?(response.last_modified) - when if_none_match - etag_matches?(response.etag) - else - false - end - end + last_modified = if_modified_since + etag = if_none_match - ONLY_ALL = [Mime::ALL].freeze + return false unless last_modified || etag + + success = true + success &&= not_modified?(response.last_modified) if last_modified + success &&= etag_matches?(response.etag) if etag + success + end # Returns the Mime type for the \format used in the request. # @@ -204,10 +205,6 @@ module ActionDispatch end end - def cache_format - parameters[:format] - end - # Returns true if the request's "X-Requested-With" header contains # "XMLHttpRequest". (The Prototype Javascript library sends this header with # every Ajax request.) diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 3e3b473178..b3ed7c9d1a 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -270,6 +270,8 @@ module ActionDispatch # :nodoc: if control.empty? headers["Cache-Control"] = DEFAULT_CACHE_CONTROL + elsif @cache_control[:no_cache] + headers["Cache-Control"] = "no-cache" else extras = control[:extras] max_age = control[:max_age] diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb new file mode 100644 index 0000000000..b9c377db2c --- /dev/null +++ b/actionpack/lib/action_dispatch/routing.rb @@ -0,0 +1,383 @@ +require 'active_support/core_ext/object/conversions' +require 'active_support/core_ext/boolean/conversions' +require 'active_support/core_ext/nil/conversions' +require 'active_support/core_ext/regexp' + +module ActionDispatch + # == Routing + # + # The routing module provides URL rewriting in native Ruby. It's a way to + # redirect incoming requests to controllers and actions. This replaces + # mod_rewrite rules. Best of all, Rails' Routing works with any web server. + # Routes are defined in <tt>config/routes.rb</tt>. + # + # Consider the following route, installed by Rails when you generate your + # application: + # + # map.connect ':controller/:action/:id' + # + # This route states that it expects requests to consist of a + # <tt>:controller</tt> followed by an <tt>:action</tt> that in turn is fed + # some <tt>:id</tt>. + # + # Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end up + # with: + # + # params = { :controller => 'blog', + # :action => 'edit', + # :id => '22' + # } + # + # Think of creating routes as drawing a map for your requests. The map tells + # them where to go based on some predefined pattern: + # + # ActionController::Routing::Routes.draw do |map| + # Pattern 1 tells some request to go to one place + # Pattern 2 tell them to go to another + # ... + # end + # + # The following symbols are special: + # + # :controller maps to your controller name + # :action maps to an action with your controllers + # + # Other names simply map to a parameter as in the case of <tt>:id</tt>. + # + # == Route priority + # + # Not all routes are created equally. Routes have priority defined by the + # order of appearance of the routes in the <tt>config/routes.rb</tt> file. The priority goes + # from top to bottom. The last route in that file is at the lowest priority + # and will be applied last. If no route matches, 404 is returned. + # + # Within blocks, the empty pattern is at the highest priority. + # In practice this works out nicely: + # + # ActionController::Routing::Routes.draw do |map| + # map.with_options :controller => 'blog' do |blog| + # blog.show '', :action => 'list' + # end + # map.connect ':controller/:action/:view' + # end + # + # In this case, invoking blog controller (with an URL like '/blog/') + # without parameters will activate the 'list' action by default. + # + # == Defaults routes and default parameters + # + # Setting a default route is straightforward in Rails - you simply append a + # Hash at the end of your mapping to set any default parameters. + # + # Example: + # + # ActionController::Routing:Routes.draw do |map| + # map.connect ':controller/:action/:id', :controller => 'blog' + # end + # + # This sets up +blog+ as the default controller if no other is specified. + # This means visiting '/' would invoke the blog controller. + # + # More formally, you can include arbitrary parameters in the route, thus: + # + # map.connect ':controller/:action/:id', :action => 'show', :page => 'Dashboard' + # + # This will pass the :page parameter to all incoming requests that match this route. + # + # Note: The default routes, as provided by the Rails generator, make all actions in every + # controller accessible via GET requests. You should consider removing them or commenting + # them out if you're using named routes and resources. + # + # == Named routes + # + # Routes can be named with the syntax <tt>map.name_of_route options</tt>, + # allowing for easy reference within your source as +name_of_route_url+ + # for the full URL and +name_of_route_path+ for the URI path. + # + # Example: + # + # # In routes.rb + # map.login 'login', :controller => 'accounts', :action => 'login' + # + # # With render, redirect_to, tests, etc. + # redirect_to login_url + # + # Arguments can be passed as well. + # + # redirect_to show_item_path(:id => 25) + # + # Use <tt>map.root</tt> as a shorthand to name a route for the root path "". + # + # # In routes.rb + # map.root :controller => 'blogs' + # + # # would recognize http://www.example.com/ as + # params = { :controller => 'blogs', :action => 'index' } + # + # # and provide these named routes + # root_url # => 'http://www.example.com/' + # root_path # => '' + # + # You can also specify an already-defined named route in your <tt>map.root</tt> call: + # + # # In routes.rb + # map.new_session :controller => 'sessions', :action => 'new' + # map.root :new_session + # + # Note: when using +with_options+, the route is simply named after the + # method you call on the block parameter rather than map. + # + # # In routes.rb + # map.with_options :controller => 'blog' do |blog| + # blog.show '', :action => 'list' + # blog.delete 'delete/:id', :action => 'delete' + # blog.edit 'edit/:id', :action => 'edit' + # end + # + # # provides named routes for show, delete, and edit + # link_to @article.title, show_path(:id => @article.id) + # + # == Pretty URLs + # + # Routes can generate pretty URLs. For example: + # + # map.connect 'articles/:year/:month/:day', + # :controller => 'articles', + # :action => 'find_by_date', + # :year => /\d{4}/, + # :month => /\d{1,2}/, + # :day => /\d{1,2}/ + # + # Using the route above, the URL "http://localhost:3000/articles/2005/11/06" + # maps to + # + # params = {:year => '2005', :month => '11', :day => '06'} + # + # == Regular Expressions and parameters + # You can specify a regular expression to define a format for a parameter. + # + # map.geocode 'geocode/:postalcode', :controller => 'geocode', + # :action => 'show', :postalcode => /\d{5}(-\d{4})?/ + # + # or, more formally: + # + # map.geocode 'geocode/:postalcode', :controller => 'geocode', + # :action => 'show', :requirements => { :postalcode => /\d{5}(-\d{4})?/ } + # + # Formats can include the 'ignorecase' and 'extended syntax' regular + # expression modifiers: + # + # map.geocode 'geocode/:postalcode', :controller => 'geocode', + # :action => 'show', :postalcode => /hx\d\d\s\d[a-z]{2}/i + # + # map.geocode 'geocode/:postalcode', :controller => 'geocode', + # :action => 'show',:requirements => { + # :postalcode => /# Postcode format + # \d{5} #Prefix + # (-\d{4})? #Suffix + # /x + # } + # + # Using the multiline match modifier will raise an ArgumentError. + # Encoding regular expression modifiers are silently ignored. The + # match will always use the default encoding or ASCII. + # + # == Route globbing + # + # Specifying <tt>*[string]</tt> as part of a rule like: + # + # map.connect '*path' , :controller => 'blog' , :action => 'unrecognized?' + # + # will glob all remaining parts of the route that were not recognized earlier. + # The globbed values are in <tt>params[:path]</tt> as an array of path segments. + # + # == Route conditions + # + # With conditions you can define restrictions on routes. Currently the only valid condition is <tt>:method</tt>. + # + # * <tt>:method</tt> - Allows you to specify which method can access the route. Possible values are <tt>:post</tt>, + # <tt>:get</tt>, <tt>:put</tt>, <tt>:delete</tt> and <tt>:any</tt>. The default value is <tt>:any</tt>, + # <tt>:any</tt> means that any method can access the route. + # + # Example: + # + # map.connect 'post/:id', :controller => 'posts', :action => 'show', + # :conditions => { :method => :get } + # map.connect 'post/:id', :controller => 'posts', :action => 'create_comment', + # :conditions => { :method => :post } + # + # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same + # URL will route to the <tt>show</tt> action. + # + # == Reloading routes + # + # You can reload routes if you feel you must: + # + # ActionController::Routing::Routes.reload + # + # This will clear all named routes and reload routes.rb if the file has been modified from + # last load. To absolutely force reloading, use <tt>reload!</tt>. + # + # == Testing Routes + # + # The two main methods for testing your routes: + # + # === +assert_routing+ + # + # def test_movie_route_properly_splits + # opts = {:controller => "plugin", :action => "checkout", :id => "2"} + # assert_routing "plugin/checkout/2", opts + # end + # + # +assert_routing+ lets you test whether or not the route properly resolves into options. + # + # === +assert_recognizes+ + # + # def test_route_has_options + # opts = {:controller => "plugin", :action => "show", :id => "12"} + # assert_recognizes opts, "/plugins/show/12" + # end + # + # Note the subtle difference between the two: +assert_routing+ tests that + # a URL fits options while +assert_recognizes+ tests that a URL + # breaks into parameters properly. + # + # In tests you can simply pass the URL or named route to +get+ or +post+. + # + # def send_to_jail + # get '/jail' + # assert_response :success + # assert_template "jail/front" + # end + # + # def goes_to_login + # get login_url + # #... + # end + # + # == View a list of all your routes + # + # Run <tt>rake routes</tt>. + # + module Routing + autoload :DeprecatedMapper, 'action_dispatch/routing/deprecated_mapper' + autoload :Mapper, 'action_dispatch/routing/mapper' + autoload :Route, 'action_dispatch/routing/route' + autoload :RouteSet, 'action_dispatch/routing/route_set' + + SEPARATORS = %w( / . ? ) + + HTTP_METHODS = [:get, :head, :post, :put, :delete, :options] + + ALLOWED_REQUIREMENTS_FOR_OPTIMISATION = [:controller, :action].to_set + + # The root paths which may contain controller files + mattr_accessor :controller_paths + self.controller_paths = [] + + # A helper module to hold URL related helpers. + module Helpers + include ActionController::PolymorphicRoutes + end + + class << self + # Expects an array of controller names as the first argument. + # Executes the passed block with only the named controllers named available. + # This method is used in internal Rails testing. + def with_controllers(names) + prior_controllers = @possible_controllers + use_controllers! names + yield + ensure + use_controllers! prior_controllers + end + + # Returns an array of paths, cleaned of double-slashes and relative path references. + # * "\\\" and "//" become "\\" or "/". + # * "/foo/bar/../config" becomes "/foo/config". + # The returned array is sorted by length, descending. + def normalize_paths(paths) + # do the hokey-pokey of path normalization... + paths = paths.collect do |path| + path = path. + gsub("//", "/"). # replace double / chars with a single + gsub("\\\\", "\\"). # replace double \ chars with a single + gsub(%r{(.)[\\/]$}, '\1') # drop final / or \ if path ends with it + + # eliminate .. paths where possible + re = %r{[^/\\]+[/\\]\.\.[/\\]} + path.gsub!(re, "") while path.match(re) + path + end + + # start with longest path, first + paths = paths.uniq.sort_by { |path| - path.length } + end + + # Returns the array of controller names currently available to ActionController::Routing. + def possible_controllers + unless @possible_controllers + @possible_controllers = [] + + paths = controller_paths.select { |path| File.directory?(path) && path != "." } + + seen_paths = Hash.new {|h, k| h[k] = true; false} + normalize_paths(paths).each do |load_path| + Dir["#{load_path}/**/*_controller.rb"].collect do |path| + next if seen_paths[path.gsub(%r{^\.[/\\]}, "")] + + controller_name = path[(load_path.length + 1)..-1] + + controller_name.gsub!(/_controller\.rb\Z/, '') + @possible_controllers << controller_name + end + end + + # remove duplicates + @possible_controllers.uniq! + end + @possible_controllers + end + + # Replaces the internal list of controllers available to ActionController::Routing with the passed argument. + # ActionController::Routing.use_controllers!([ "posts", "comments", "admin/comments" ]) + def use_controllers!(controller_names) + @possible_controllers = controller_names + end + + # Returns a controller path for a new +controller+ based on a +previous+ controller path. + # Handles 4 scenarios: + # + # * stay in the previous controller: + # controller_relative_to( nil, "groups/discussion" ) # => "groups/discussion" + # + # * stay in the previous namespace: + # controller_relative_to( "posts", "groups/discussion" ) # => "groups/posts" + # + # * forced move to the root namespace: + # controller_relative_to( "/posts", "groups/discussion" ) # => "posts" + # + # * previous namespace is root: + # controller_relative_to( "posts", "anything_with_no_slashes" ) # =>"posts" + # + def controller_relative_to(controller, previous) + if controller.nil? then previous + elsif controller[0] == ?/ then controller[1..-1] + elsif %r{^(.*)/} =~ previous then "#{$1}/#{controller}" + else controller + end + end + end + + ActiveSupport::Inflector.module_eval do + # Ensures that routes are reloaded when Rails inflections are updated. + def inflections_with_route_reloading(&block) + returning(inflections_without_route_reloading(&block)) { + ActionDispatch::Routing::Routes.reload! if block_given? + } + end + + alias_method_chain :inflections, :route_reloading + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb new file mode 100644 index 0000000000..0564ba9797 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb @@ -0,0 +1,878 @@ +module ActionDispatch + module Routing + # Mapper instances are used to build routes. The object passed to the draw + # block in config/routes.rb is a Mapper instance. + # + # Mapper instances have relatively few instance methods, in order to avoid + # clashes with named routes. + # + # == Overview + # + # ActionController::Resources are a way of defining RESTful \resources. A RESTful \resource, in basic terms, + # is something that can be pointed at and it will respond with a representation of the data requested. + # In real terms this could mean a user with a browser requests an HTML page, or that a desktop application + # requests XML data. + # + # RESTful design is based on the assumption that there are four generic verbs that a user of an + # application can request from a \resource (the noun). + # + # \Resources can be requested using four basic HTTP verbs (GET, POST, PUT, DELETE), the method used + # denotes the type of action that should take place. + # + # === The Different Methods and their Usage + # + # * GET - Requests for a \resource, no saving or editing of a \resource should occur in a GET request. + # * POST - Creation of \resources. + # * PUT - Editing of attributes on a \resource. + # * DELETE - Deletion of a \resource. + # + # === Examples + # + # # A GET request on the Posts resource is asking for all Posts + # GET /posts + # + # # A GET request on a single Post resource is asking for that particular Post + # GET /posts/1 + # + # # A POST request on the Posts resource is asking for a Post to be created with the supplied details + # POST /posts # with => { :post => { :title => "My Whizzy New Post", :body => "I've got a brand new combine harvester" } } + # + # # A PUT request on a single Post resource is asking for a Post to be updated + # PUT /posts # with => { :id => 1, :post => { :title => "Changed Whizzy Title" } } + # + # # A DELETE request on a single Post resource is asking for it to be deleted + # DELETE /posts # with => { :id => 1 } + # + # By using the REST convention, users of our application can assume certain things about how the data + # is requested and how it is returned. Rails simplifies the routing part of RESTful design by + # supplying you with methods to create them in your routes.rb file. + # + # Read more about REST at http://en.wikipedia.org/wiki/Representational_State_Transfer + class DeprecatedMapper #:doc: + def initialize(set) #:nodoc: + @set = set + end + + # Create an unnamed route with the provided +path+ and +options+. See + # ActionDispatch::Routing for an introduction to routes. + def connect(path, options = {}) + options = options.dup + + if conditions = options.delete(:conditions) + conditions = conditions.dup + method = [conditions.delete(:method)].flatten.compact + method.map! { |m| + m = m.to_s.upcase + + if m == "HEAD" + raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers" + end + + unless HTTP_METHODS.include?(m.downcase.to_sym) + raise ArgumentError, "Invalid HTTP method specified in route conditions" + end + + m + } + + if method.length > 1 + method = Regexp.union(*method) + elsif method.length == 1 + method = method.first + else + method = nil + end + end + + path_prefix = options.delete(:path_prefix) + name_prefix = options.delete(:name_prefix) + namespace = options.delete(:namespace) + + name = options.delete(:_name) + name = "#{name_prefix}#{name}" if name_prefix + + requirements = options.delete(:requirements) || {} + defaults = options.delete(:defaults) || {} + options.each do |k, v| + if v.is_a?(Regexp) + if value = options.delete(k) + requirements[k.to_sym] = value + end + else + value = options.delete(k) + defaults[k.to_sym] = value.is_a?(Symbol) ? value : value.to_param + end + end + + requirements.each do |_, requirement| + if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" + end + if requirement.multiline? + raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" + end + end + + possible_names = Routing.possible_controllers.collect { |n| Regexp.escape(n) } + requirements[:controller] ||= Regexp.union(*possible_names) + + if defaults[:controller] + defaults[:action] ||= 'index' + defaults[:controller] = defaults[:controller].to_s + defaults[:controller] = "#{namespace}#{defaults[:controller]}" if namespace + end + + if defaults[:action] + defaults[:action] = defaults[:action].to_s + end + + if path.is_a?(String) + path = "#{path_prefix}/#{path}" if path_prefix + path = path.gsub('.:format', '(.:format)') + path = optionalize_trailing_dynamic_segments(path, requirements, defaults) + glob = $1.to_sym if path =~ /\/\*(\w+)$/ + path = ::Rack::Mount::Utils.normalize_path(path) + + if glob && !defaults[glob].blank? + raise ActionController::RoutingError, "paths cannot have non-empty default values" + end + end + + app = Routing::RouteSet::Dispatcher.new(:defaults => defaults, :glob => glob) + + conditions = {} + conditions[:request_method] = method if method + conditions[:path_info] = path if path + + @set.add_route(app, conditions, requirements, defaults, name) + end + + def optionalize_trailing_dynamic_segments(path, requirements, defaults) #:nodoc: + path = (path =~ /^\//) ? path.dup : "/#{path}" + optional, segments = true, [] + + required_segments = requirements.keys + required_segments -= defaults.keys.compact + + old_segments = path.split('/') + old_segments.shift + length = old_segments.length + + old_segments.reverse.each_with_index do |segment, index| + required_segments.each do |required| + if segment =~ /#{required}/ + optional = false + break + end + end + + if optional + if segment == ":id" && segments.include?(":action") + optional = false + elsif segment == ":controller" || segment == ":action" || segment == ":id" + # Ignore + elsif !(segment =~ /^:\w+$/) && + !(segment =~ /^:\w+\(\.:format\)$/) + optional = false + elsif segment =~ /^:(\w+)$/ + if defaults.has_key?($1.to_sym) + defaults.delete($1.to_sym) + else + optional = false + end + end + end + + if optional && index < length - 1 + segments.unshift('(/', segment) + segments.push(')') + elsif optional + segments.unshift('/(', segment) + segments.push(')') + else + segments.unshift('/', segment) + end + end + + segments.join + end + private :optionalize_trailing_dynamic_segments + + # Creates a named route called "root" for matching the root level request. + def root(options = {}) + if options.is_a?(Symbol) + if source_route = @set.named_routes.routes[options] + options = source_route.defaults.merge({ :conditions => source_route.conditions }) + end + end + named_route("root", '', options) + end + + def named_route(name, path, options = {}) #:nodoc: + options[:_name] = name + connect(path, options) + end + + # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model. + # Example: + # + # map.namespace(:admin) do |admin| + # admin.resources :products, + # :has_many => [ :tags, :images, :variants ] + # end + # + # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController. + # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for + # Admin::TagsController. + def namespace(name, options = {}, &block) + if options[:namespace] + with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block) + else + with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block) + end + end + + def method_missing(route_name, *args, &proc) #:nodoc: + super unless args.length >= 1 && proc.nil? + named_route(route_name, *args) + end + + INHERITABLE_OPTIONS = :namespace, :shallow + + class Resource #:nodoc: + DEFAULT_ACTIONS = :index, :create, :new, :edit, :show, :update, :destroy + + attr_reader :collection_methods, :member_methods, :new_methods + attr_reader :path_prefix, :name_prefix, :path_segment + attr_reader :plural, :singular + attr_reader :options + + def initialize(entities, options) + @plural ||= entities + @singular ||= options[:singular] || plural.to_s.singularize + @path_segment = options.delete(:as) || @plural + + @options = options + + arrange_actions + add_default_actions + set_allowed_actions + set_prefixes + end + + def controller + @controller ||= "#{options[:namespace]}#{(options[:controller] || plural).to_s}" + end + + def requirements(with_id = false) + @requirements ||= @options[:requirements] || {} + @id_requirement ||= { :id => @requirements.delete(:id) || /[^#{Routing::SEPARATORS.join}]+/ } + + with_id ? @requirements.merge(@id_requirement) : @requirements + end + + def conditions + @conditions ||= @options[:conditions] || {} + end + + def path + @path ||= "#{path_prefix}/#{path_segment}" + end + + def new_path + new_action = self.options[:path_names][:new] if self.options[:path_names] + new_action ||= ActionController::Base.resources_path_names[:new] + @new_path ||= "#{path}/#{new_action}" + end + + def shallow_path_prefix + @shallow_path_prefix ||= @options[:shallow] ? @options[:namespace].try(:sub, /\/$/, '') : path_prefix + end + + def member_path + @member_path ||= "#{shallow_path_prefix}/#{path_segment}/:id" + end + + def nesting_path_prefix + @nesting_path_prefix ||= "#{shallow_path_prefix}/#{path_segment}/:#{singular}_id" + end + + def shallow_name_prefix + @shallow_name_prefix ||= @options[:shallow] ? @options[:namespace].try(:gsub, /\//, '_') : name_prefix + end + + def nesting_name_prefix + "#{shallow_name_prefix}#{singular}_" + end + + def action_separator + @action_separator ||= ActionController::Base.resource_action_separator + end + + def uncountable? + @singular.to_s == @plural.to_s + end + + def has_action?(action) + !DEFAULT_ACTIONS.include?(action) || action_allowed?(action) + end + + protected + def arrange_actions + @collection_methods = arrange_actions_by_methods(options.delete(:collection)) + @member_methods = arrange_actions_by_methods(options.delete(:member)) + @new_methods = arrange_actions_by_methods(options.delete(:new)) + end + + def add_default_actions + add_default_action(member_methods, :get, :edit) + add_default_action(new_methods, :get, :new) + end + + def set_allowed_actions + only, except = @options.values_at(:only, :except) + @allowed_actions ||= {} + + if only == :all || except == :none + only = nil + except = [] + elsif only == :none || except == :all + only = [] + except = nil + end + + if only + @allowed_actions[:only] = Array(only).map {|a| a.to_sym } + elsif except + @allowed_actions[:except] = Array(except).map {|a| a.to_sym } + end + end + + def action_allowed?(action) + only, except = @allowed_actions.values_at(:only, :except) + (!only || only.include?(action)) && (!except || !except.include?(action)) + end + + def set_prefixes + @path_prefix = options.delete(:path_prefix) + @name_prefix = options.delete(:name_prefix) + end + + def arrange_actions_by_methods(actions) + (actions || {}).inject({}) do |flipped_hash, (key, value)| + (flipped_hash[value] ||= []) << key + flipped_hash + end + end + + def add_default_action(collection, method, action) + (collection[method] ||= []).unshift(action) + end + end + + class SingletonResource < Resource #:nodoc: + def initialize(entity, options) + @singular = @plural = entity + options[:controller] ||= @singular.to_s.pluralize + super + end + + alias_method :shallow_path_prefix, :path_prefix + alias_method :shallow_name_prefix, :name_prefix + alias_method :member_path, :path + alias_method :nesting_path_prefix, :path + end + + # Creates named routes for implementing verb-oriented controllers + # for a collection \resource. + # + # For example: + # + # map.resources :messages + # + # will map the following actions in the corresponding controller: + # + # class MessagesController < ActionController::Base + # # GET messages_url + # def index + # # return all messages + # end + # + # # GET new_message_url + # def new + # # return an HTML form for describing a new message + # end + # + # # POST messages_url + # def create + # # create a new message + # end + # + # # GET message_url(:id => 1) + # def show + # # find and return a specific message + # end + # + # # GET edit_message_url(:id => 1) + # def edit + # # return an HTML form for editing a specific message + # end + # + # # PUT message_url(:id => 1) + # def update + # # find and update a specific message + # end + # + # # DELETE message_url(:id => 1) + # def destroy + # # delete a specific message + # end + # end + # + # Along with the routes themselves, +resources+ generates named routes for use in + # controllers and views. <tt>map.resources :messages</tt> produces the following named routes and helpers: + # + # Named Route Helpers + # ============ ===================================================== + # messages messages_url, hash_for_messages_url, + # messages_path, hash_for_messages_path + # + # message message_url(id), hash_for_message_url(id), + # message_path(id), hash_for_message_path(id) + # + # new_message new_message_url, hash_for_new_message_url, + # new_message_path, hash_for_new_message_path + # + # edit_message edit_message_url(id), hash_for_edit_message_url(id), + # edit_message_path(id), hash_for_edit_message_path(id) + # + # You can use these helpers instead of +url_for+ or methods that take +url_for+ parameters. For example: + # + # redirect_to :controller => 'messages', :action => 'index' + # # and + # <%= link_to "edit this message", :controller => 'messages', :action => 'edit', :id => @message.id %> + # + # now become: + # + # redirect_to messages_url + # # and + # <%= link_to "edit this message", edit_message_url(@message) # calls @message.id automatically + # + # Since web browsers don't support the PUT and DELETE verbs, you will need to add a parameter '_method' to your + # form tags. The form helpers make this a little easier. For an update form with a <tt>@message</tt> object: + # + # <%= form_tag message_path(@message), :method => :put %> + # + # or + # + # <% form_for :message, @message, :url => message_path(@message), :html => {:method => :put} do |f| %> + # + # or + # + # <% form_for @message do |f| %> + # + # which takes into account whether <tt>@message</tt> is a new record or not and generates the + # path and method accordingly. + # + # The +resources+ method accepts the following options to customize the resulting routes: + # * <tt>:collection</tt> - Add named routes for other actions that operate on the collection. + # Takes a hash of <tt>#{action} => #{method}</tt>, where method is <tt>:get</tt>/<tt>:post</tt>/<tt>:put</tt>/<tt>:delete</tt>, + # an array of any of the previous, or <tt>:any</tt> if the method does not matter. + # These routes map to a URL like /messages/rss, with a route of +rss_messages_url+. + # * <tt>:member</tt> - Same as <tt>:collection</tt>, but for actions that operate on a specific member. + # * <tt>:new</tt> - Same as <tt>:collection</tt>, but for actions that operate on the new \resource action. + # * <tt>:controller</tt> - Specify the controller name for the routes. + # * <tt>:singular</tt> - Specify the singular name used in the member routes. + # * <tt>:requirements</tt> - Set custom routing parameter requirements; this is a hash of either + # regular expressions (which must match for the route to match) or extra parameters. For example: + # + # map.resource :profile, :path_prefix => ':name', :requirements => { :name => /[a-zA-Z]+/, :extra => 'value' } + # + # will only match if the first part is alphabetic, and will pass the parameter :extra to the controller. + # * <tt>:conditions</tt> - Specify custom routing recognition conditions. \Resources sets the <tt>:method</tt> value for the method-specific routes. + # * <tt>:as</tt> - Specify a different \resource name to use in the URL path. For example: + # # products_path == '/productos' + # map.resources :products, :as => 'productos' do |product| + # # product_reviews_path(product) == '/productos/1234/comentarios' + # product.resources :product_reviews, :as => 'comentarios' + # end + # + # * <tt>:has_one</tt> - Specify nested \resources, this is a shorthand for mapping singleton \resources beneath the current. + # * <tt>:has_many</tt> - Same has <tt>:has_one</tt>, but for plural \resources. + # + # You may directly specify the routing association with +has_one+ and +has_many+ like: + # + # map.resources :notes, :has_one => :author, :has_many => [:comments, :attachments] + # + # This is the same as: + # + # map.resources :notes do |notes| + # notes.resource :author + # notes.resources :comments + # notes.resources :attachments + # end + # + # * <tt>:path_names</tt> - Specify different path names for the actions. For example: + # # new_products_path == '/productos/nuevo' + # # bids_product_path(1) == '/productos/1/licitacoes' + # map.resources :products, :as => 'productos', :member => { :bids => :get }, :path_names => { :new => 'nuevo', :bids => 'licitacoes' } + # + # You can also set default action names from an environment, like this: + # config.action_controller.resources_path_names = { :new => 'nuevo', :edit => 'editar' } + # + # * <tt>:path_prefix</tt> - Set a prefix to the routes with required route variables. + # + # Weblog comments usually belong to a post, so you might use +resources+ like: + # + # map.resources :articles + # map.resources :comments, :path_prefix => '/articles/:article_id' + # + # You can nest +resources+ calls to set this automatically: + # + # map.resources :articles do |article| + # article.resources :comments + # end + # + # The comment \resources work the same, but must now include a value for <tt>:article_id</tt>. + # + # article_comments_url(@article) + # article_comment_url(@article, @comment) + # + # article_comments_url(:article_id => @article) + # article_comment_url(:article_id => @article, :id => @comment) + # + # If you don't want to load all objects from the database you might want to use the <tt>article_id</tt> directly: + # + # articles_comments_url(@comment.article_id, @comment) + # + # * <tt>:name_prefix</tt> - Define a prefix for all generated routes, usually ending in an underscore. + # Use this if you have named routes that may clash. + # + # map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_' + # map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_' + # + # You may also use <tt>:name_prefix</tt> to override the generic named routes in a nested \resource: + # + # map.resources :articles do |article| + # article.resources :comments, :name_prefix => nil + # end + # + # This will yield named \resources like so: + # + # comments_url(@article) + # comment_url(@article, @comment) + # + # * <tt>:shallow</tt> - If true, paths for nested resources which reference a specific member + # (ie. those with an :id parameter) will not use the parent path prefix or name prefix. + # + # The <tt>:shallow</tt> option is inherited by any nested resource(s). + # + # For example, 'users', 'posts' and 'comments' all use shallow paths with the following nested resources: + # + # map.resources :users, :shallow => true do |user| + # user.resources :posts do |post| + # post.resources :comments + # end + # end + # # --> GET /users/1/posts (maps to the PostsController#index action as usual) + # # also adds the usual named route called "user_posts" + # # --> GET /posts/2 (maps to the PostsController#show action as if it were not nested) + # # also adds the named route called "post" + # # --> GET /posts/2/comments (maps to the CommentsController#index action) + # # also adds the named route called "post_comments" + # # --> GET /comments/2 (maps to the CommentsController#show action as if it were not nested) + # # also adds the named route called "comment" + # + # You may also use <tt>:shallow</tt> in combination with the +has_one+ and +has_many+ shorthand notations like: + # + # map.resources :users, :has_many => { :posts => :comments }, :shallow => true + # + # * <tt>:only</tt> and <tt>:except</tt> - Specify which of the seven default actions should be routed to. + # + # <tt>:only</tt> and <tt>:except</tt> may be set to <tt>:all</tt>, <tt>:none</tt>, an action name or a + # list of action names. By default, routes are generated for all seven actions. + # + # For example: + # + # map.resources :posts, :only => [:index, :show] do |post| + # post.resources :comments, :except => [:update, :destroy] + # end + # # --> GET /posts (maps to the PostsController#index action) + # # --> POST /posts (fails) + # # --> GET /posts/1 (maps to the PostsController#show action) + # # --> DELETE /posts/1 (fails) + # # --> POST /posts/1/comments (maps to the CommentsController#create action) + # # --> PUT /posts/1/comments/1 (fails) + # + # If <tt>map.resources</tt> is called with multiple resources, they all get the same options applied. + # + # Examples: + # + # map.resources :messages, :path_prefix => "/thread/:thread_id" + # # --> GET /thread/7/messages/1 + # + # map.resources :messages, :collection => { :rss => :get } + # # --> GET /messages/rss (maps to the #rss action) + # # also adds a named route called "rss_messages" + # + # map.resources :messages, :member => { :mark => :post } + # # --> POST /messages/1/mark (maps to the #mark action) + # # also adds a named route called "mark_message" + # + # map.resources :messages, :new => { :preview => :post } + # # --> POST /messages/new/preview (maps to the #preview action) + # # also adds a named route called "preview_new_message" + # + # map.resources :messages, :new => { :new => :any, :preview => :post } + # # --> POST /messages/new/preview (maps to the #preview action) + # # also adds a named route called "preview_new_message" + # # --> /messages/new can be invoked via any request method + # + # map.resources :messages, :controller => "categories", + # :path_prefix => "/category/:category_id", + # :name_prefix => "category_" + # # --> GET /categories/7/messages/1 + # # has named route "category_message" + # + # The +resources+ method sets HTTP method restrictions on the routes it generates. For example, making an + # HTTP POST on <tt>new_message_url</tt> will raise a RoutingError exception. The default route in + # <tt>config/routes.rb</tt> overrides this and allows invalid HTTP methods for \resource routes. + def resources(*entities, &block) + options = entities.extract_options! + entities.each { |entity| map_resource(entity, options.dup, &block) } + end + + # Creates named routes for implementing verb-oriented controllers for a singleton \resource. + # A singleton \resource is global to its current context. For unnested singleton \resources, + # the \resource is global to the current user visiting the application, such as a user's + # <tt>/account</tt> profile. For nested singleton \resources, the \resource is global to its parent + # \resource, such as a <tt>projects</tt> \resource that <tt>has_one :project_manager</tt>. + # The <tt>project_manager</tt> should be mapped as a singleton \resource under <tt>projects</tt>: + # + # map.resources :projects do |project| + # project.resource :project_manager + # end + # + # See +resources+ for general conventions. These are the main differences: + # * A singular name is given to <tt>map.resource</tt>. The default controller name is still taken from the plural name. + # * To specify a custom plural name, use the <tt>:plural</tt> option. There is no <tt>:singular</tt> option. + # * No default index route is created for the singleton \resource controller. + # * When nesting singleton \resources, only the singular name is used as the path prefix (example: 'account/messages/1') + # + # For example: + # + # map.resource :account + # + # maps these actions in the Accounts controller: + # + # class AccountsController < ActionController::Base + # # GET new_account_url + # def new + # # return an HTML form for describing the new account + # end + # + # # POST account_url + # def create + # # create an account + # end + # + # # GET account_url + # def show + # # find and return the account + # end + # + # # GET edit_account_url + # def edit + # # return an HTML form for editing the account + # end + # + # # PUT account_url + # def update + # # find and update the account + # end + # + # # DELETE account_url + # def destroy + # # delete the account + # end + # end + # + # Along with the routes themselves, +resource+ generates named routes for + # use in controllers and views. <tt>map.resource :account</tt> produces + # these named routes and helpers: + # + # Named Route Helpers + # ============ ============================================= + # account account_url, hash_for_account_url, + # account_path, hash_for_account_path + # + # new_account new_account_url, hash_for_new_account_url, + # new_account_path, hash_for_new_account_path + # + # edit_account edit_account_url, hash_for_edit_account_url, + # edit_account_path, hash_for_edit_account_path + def resource(*entities, &block) + options = entities.extract_options! + entities.each { |entity| map_singleton_resource(entity, options.dup, &block) } + end + + private + def map_resource(entities, options = {}, &block) + resource = Resource.new(entities, options) + + with_options :controller => resource.controller do |map| + map_associations(resource, options) + + if block_given? + with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) + end + + map_collection_actions(map, resource) + map_default_collection_actions(map, resource) + map_new_actions(map, resource) + map_member_actions(map, resource) + end + end + + def map_singleton_resource(entities, options = {}, &block) + resource = SingletonResource.new(entities, options) + + with_options :controller => resource.controller do |map| + map_associations(resource, options) + + if block_given? + with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) + end + + map_collection_actions(map, resource) + map_new_actions(map, resource) + map_member_actions(map, resource) + map_default_singleton_actions(map, resource) + end + end + + def map_associations(resource, options) + map_has_many_associations(resource, options.delete(:has_many), options) if options[:has_many] + + path_prefix = "#{options.delete(:path_prefix)}#{resource.nesting_path_prefix}" + name_prefix = "#{options.delete(:name_prefix)}#{resource.nesting_name_prefix}" + + Array(options[:has_one]).each do |association| + resource(association, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => path_prefix, :name_prefix => name_prefix)) + end + end + + def map_has_many_associations(resource, associations, options) + case associations + when Hash + associations.each do |association,has_many| + map_has_many_associations(resource, association, options.merge(:has_many => has_many)) + end + when Array + associations.each do |association| + map_has_many_associations(resource, association, options) + end + when Symbol, String + resources(associations, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :has_many => options[:has_many])) + else + end + end + + def map_collection_actions(map, resource) + resource.collection_methods.each do |method, actions| + actions.each do |action| + [method].flatten.each do |m| + action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) + action_path ||= action + + map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.name_prefix}#{resource.plural}", m) + end + end + end + end + + def map_default_collection_actions(map, resource) + index_route_name = "#{resource.name_prefix}#{resource.plural}" + + if resource.uncountable? + index_route_name << "_index" + end + + map_resource_routes(map, resource, :index, resource.path, index_route_name) + map_resource_routes(map, resource, :create, resource.path, index_route_name) + end + + def map_default_singleton_actions(map, resource) + map_resource_routes(map, resource, :create, resource.path, "#{resource.shallow_name_prefix}#{resource.singular}") + end + + def map_new_actions(map, resource) + resource.new_methods.each do |method, actions| + actions.each do |action| + route_path = resource.new_path + route_name = "new_#{resource.name_prefix}#{resource.singular}" + + unless action == :new + route_path = "#{route_path}#{resource.action_separator}#{action}" + route_name = "#{action}_#{route_name}" + end + + map_resource_routes(map, resource, action, route_path, route_name, method) + end + end + end + + def map_member_actions(map, resource) + resource.member_methods.each do |method, actions| + actions.each do |action| + [method].flatten.each do |m| + action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) + action_path ||= ActionController::Base.resources_path_names[action] || action + + map_resource_routes(map, resource, action, "#{resource.member_path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", m, { :force_id => true }) + end + end + end + + route_path = "#{resource.shallow_name_prefix}#{resource.singular}" + map_resource_routes(map, resource, :show, resource.member_path, route_path) + map_resource_routes(map, resource, :update, resource.member_path, route_path) + map_resource_routes(map, resource, :destroy, resource.member_path, route_path) + end + + def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil, resource_options = {} ) + if resource.has_action?(action) + action_options = action_options_for(action, resource, method, resource_options) + formatted_route_path = "#{route_path}.:format" + + if route_name && @set.named_routes[route_name.to_sym].nil? + map.named_route(route_name, formatted_route_path, action_options) + else + map.connect(formatted_route_path, action_options) + end + end + end + + def add_conditions_for(conditions, method) + returning({:conditions => conditions.dup}) do |options| + options[:conditions][:method] = method unless method == :any + end + end + + def action_options_for(action, resource, method = nil, resource_options = {}) + default_options = { :action => action.to_s } + require_id = !resource.kind_of?(SingletonResource) + force_id = resource_options[:force_id] && !resource.kind_of?(SingletonResource) + + case default_options[:action] + when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements) + when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements) + when "show", "edit"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id)) + when "update"; default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id)) + when "destroy"; default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id)) + else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements(force_id)) + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb new file mode 100644 index 0000000000..d6d822842b --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -0,0 +1,315 @@ +module ActionDispatch + module Routing + class Mapper + module Resources + def resource(*resources, &block) + options = resources.last.is_a?(Hash) ? resources.pop : {} + + if resources.length > 1 + raise ArgumentError if block_given? + resources.each { |r| resource(r, options) } + return self + end + + resource = resources.pop + + if @scope[:scope_level] == :resources + member do + resource(resource, options, &block) + end + return self + end + + controller(resource) do + namespace(resource) do + with_scope_level(:resource) do + yield if block_given? + + get "", :to => :show + post "", :to => :create + put "", :to => :update + delete "", :to => :destory + end + end + end + + self + end + + def resources(*resources, &block) + options = resources.last.is_a?(Hash) ? resources.pop : {} + + if resources.length > 1 + raise ArgumentError if block_given? + resources.each { |r| resources(r, options) } + return self + end + + resource = resources.pop + + if @scope[:scope_level] == :resources + member do + resources(resource, options, &block) + end + return self + end + + controller(resource) do + namespace(resource) do + with_scope_level(:resources) do + yield if block_given? + + member do + get "", :to => :show + put "", :to => :update + delete "", :to => :destory + get "edit", :to => :edit + end + + collection do + get "", :to => :index + post "", :to => :create + get "new", :to => :new + end + end + end + end + + self + end + + def collection + unless @scope[:scope_level] == :resources + raise ArgumentError, "can't use collection outside resources scope" + end + + with_scope_level(:collection) do + yield + end + end + + def member + unless @scope[:scope_level] == :resources + raise ArgumentError, "can't use member outside resources scope" + end + + with_scope_level(:member) do + scope(":id") do + yield + end + end + end + + def match(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + args.push(options) + + case options.delete(:on) + when :collection + return collection { match(*args) } + when :member + return member { match(*args) } + end + + if @scope[:scope_level] == :resources + raise ArgumentError, "can't define route directly in resources scope" + end + + super + end + + private + def with_scope_level(kind) + old, @scope[:scope_level] = @scope[:scope_level], kind + yield + ensure + @scope[:scope_level] = old + end + end + + module Scoping + def scope(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + + constraints = options.delete(:constraints) || {} + unless constraints.is_a?(Hash) + block, constraints = constraints, {} + end + constraints, @scope[:constraints] = @scope[:constraints], (@scope[:constraints] || {}).merge(constraints) + blocks, @scope[:blocks] = @scope[:blocks], (@scope[:blocks] || []) + [block] + + options, @scope[:options] = @scope[:options], (@scope[:options] || {}).merge(options) + + path_set = controller_set = false + + case args.first + when String + path_set = true + path = args.first + path, @scope[:path] = @scope[:path], "#{@scope[:path]}#{Rack::Mount::Utils.normalize_path(path)}" + when Symbol + controller_set = true + controller = args.first + controller, @scope[:controller] = @scope[:controller], controller + end + + yield + + self + ensure + @scope[:path] = path if path_set + @scope[:controller] = controller if controller_set + @scope[:options] = options + @scope[:blocks] = blocks + @scope[:constraints] = constraints + end + + def controller(controller) + scope(controller.to_sym) { yield } + end + + def namespace(path) + scope(path.to_s) { yield } + end + + def constraints(constraints = {}) + scope(:constraints => constraints) { yield } + end + end + + class Constraints + def initialize(app, constraints = []) + @app, @constraints = app, constraints + end + + def call(env) + req = Rack::Request.new(env) + + @constraints.each { |constraint| + if constraint.respond_to?(:matches?) && !constraint.matches?(req) + return Rack::Mount::Const::EXPECTATION_FAILED_RESPONSE + elsif constraint.respond_to?(:call) && !constraint.call(req) + return Rack::Mount::Const::EXPECTATION_FAILED_RESPONSE + end + } + + @app.call(env) + end + end + + def initialize(set) + @set = set + @scope = {} + + extend Scoping + extend Resources + end + + def get(*args, &block) + map_method(:get, *args, &block) + end + + def post(*args, &block) + map_method(:post, *args, &block) + end + + def put(*args, &block) + map_method(:put, *args, &block) + end + + def delete(*args, &block) + map_method(:delete, *args, &block) + end + + def match(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + + if args.length > 1 + args.each { |path| match(path, options) } + return self + end + + if args.first.is_a?(Symbol) + return match(args.first.to_s, options.merge(:to => args.first.to_sym)) + end + + path = args.first + + options = (@scope[:options] || {}).merge(options) + conditions, defaults = {}, {} + + path = nil if path == "" + path = Rack::Mount::Utils.normalize_path(path) if path + path = "#{@scope[:path]}#{path}" if @scope[:path] + + raise ArgumentError, "path is required" unless path + + constraints = options[:constraints] || {} + unless constraints.is_a?(Hash) + block, constraints = constraints, {} + end + blocks = ((@scope[:blocks] || []) + [block]).compact + constraints = (@scope[:constraints] || {}).merge(constraints) + options.each { |k, v| constraints[k] = v if v.is_a?(Regexp) } + + conditions[:path_info] = path + requirements = constraints.dup + + path_regexp = Rack::Mount::Strexp.compile(path, constraints, SEPARATORS) + segment_keys = Rack::Mount::RegexpWithNamedGroups.new(path_regexp).names + constraints.reject! { |k, v| segment_keys.include?(k.to_s) } + conditions.merge!(constraints) + + if via = options[:via] + via = Array(via).map { |m| m.to_s.upcase } + conditions[:request_method] = Regexp.union(*via) + end + + defaults[:controller] = @scope[:controller].to_s if @scope[:controller] + + if options[:to].respond_to?(:call) + app = options[:to] + defaults.delete(:controller) + defaults.delete(:action) + elsif options[:to].is_a?(String) + defaults[:controller], defaults[:action] = options[:to].split('#') + elsif options[:to].is_a?(Symbol) + defaults[:action] = options[:to].to_s + end + app ||= Routing::RouteSet::Dispatcher.new(:defaults => defaults) + + if app.is_a?(Routing::RouteSet::Dispatcher) + unless defaults.include?(:controller) || segment_keys.include?("controller") + raise ArgumentError, "missing :controller" + end + unless defaults.include?(:action) || segment_keys.include?("action") + raise ArgumentError, "missing :action" + end + end + + app = Constraints.new(app, blocks) if blocks.any? + @set.add_route(app, conditions, requirements, defaults, options[:as]) + + self + end + + def redirect(path, options = {}) + status = options[:status] || 301 + lambda { |env| + req = Rack::Request.new(env) + url = req.scheme + '://' + req.host + path + [status, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently']] + } + end + + private + def map_method(method, *args, &block) + options = args.last.is_a?(Hash) ? args.pop : {} + options[:via] = method + args.push(options) + match(*args, &block) + self + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/route.rb b/actionpack/lib/action_dispatch/routing/route.rb new file mode 100644 index 0000000000..f1431e7a37 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/route.rb @@ -0,0 +1,49 @@ +module ActionDispatch + module Routing + class Route #:nodoc: + attr_reader :app, :conditions, :defaults, :name + attr_reader :path, :requirements + + def initialize(app, conditions = {}, requirements = {}, defaults = {}, name = nil) + @app = app + @defaults = defaults + @name = name + + @requirements = requirements.merge(defaults) + @requirements.delete(:controller) if @requirements[:controller].is_a?(Regexp) + @requirements.delete_if { |k, v| + v == Regexp.compile("[^#{SEPARATORS.join}]+") + } + + if path = conditions[:path_info] + @path = path + conditions[:path_info] = ::Rack::Mount::Strexp.compile(path, requirements, SEPARATORS) + end + + @conditions = conditions.inject({}) { |h, (k, v)| + h[k] = Rack::Mount::RegexpWithNamedGroups.new(v) + h + } + end + + def verb + if method = conditions[:request_method] + case method + when Regexp + method.source.upcase + else + method.to_s.upcase + end + end + end + + def segment_keys + @segment_keys ||= conditions[:path_info].names.compact.map { |key| key.to_sym } + end + + def to_a + [@app, @conditions, @defaults, @name] + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb new file mode 100644 index 0000000000..93617e826d --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -0,0 +1,492 @@ +require 'rack/mount' +require 'forwardable' + +module ActionDispatch + module Routing + class RouteSet #:nodoc: + NotFound = lambda { |env| + raise ActionController::RoutingError, "No route matches #{env[::Rack::Mount::Const::PATH_INFO].inspect} with #{env.inspect}" + } + + PARAMETERS_KEY = 'action_dispatch.request.path_parameters' + + class Dispatcher + def initialize(options = {}) + defaults = options[:defaults] + @glob_param = options.delete(:glob) + end + + def call(env) + params = env[PARAMETERS_KEY] + merge_default_action!(params) + split_glob_param!(params) if @glob_param + params.each { |key, value| params[key] = URI.unescape(value) if value.is_a?(String) } + + if env['action_controller.recognize'] + [200, {}, params] + else + controller = controller(params) + controller.action(params[:action]).call(env) + end + end + + private + def controller(params) + if params && params.has_key?(:controller) + controller = "#{params[:controller].camelize}Controller" + ActiveSupport::Inflector.constantize(controller) + end + end + + def merge_default_action!(params) + params[:action] ||= 'index' + end + + def split_glob_param!(params) + params[@glob_param] = params[@glob_param].split('/').map { |v| URI.unescape(v) } + end + end + + + # A NamedRouteCollection instance is a collection of named routes, and also + # maintains an anonymous module that can be used to install helpers for the + # named routes. + class NamedRouteCollection #:nodoc: + include Enumerable + attr_reader :routes, :helpers + + def initialize + clear! + end + + def clear! + @routes = {} + @helpers = [] + + @module ||= Module.new + @module.instance_methods.each do |selector| + @module.class_eval { remove_method selector } + end + end + + def add(name, route) + routes[name.to_sym] = route + define_named_route_methods(name, route) + end + + def get(name) + routes[name.to_sym] + end + + alias []= add + alias [] get + alias clear clear! + + def each + routes.each { |name, route| yield name, route } + self + end + + def names + routes.keys + end + + def length + routes.length + end + + def reset! + old_routes = routes.dup + clear! + old_routes.each do |name, route| + add(name, route) + end + end + + def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false) + reset! if regenerate + Array(destinations).each do |dest| + dest.__send__(:include, @module) + end + end + + private + def url_helper_name(name, kind = :url) + :"#{name}_#{kind}" + end + + def hash_access_name(name, kind = :url) + :"hash_for_#{name}_#{kind}" + end + + def define_named_route_methods(name, route) + {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts| + hash = route.defaults.merge(:use_route => name).merge(opts) + define_hash_access route, name, kind, hash + define_url_helper route, name, kind, hash + end + end + + def named_helper_module_eval(code, *args) + @module.module_eval(code, *args) + end + + def define_hash_access(route, name, kind, options) + selector = hash_access_name(name, kind) + named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks + def #{selector}(options = nil) # def hash_for_users_url(options = nil) + options ? #{options.inspect}.merge(options) : #{options.inspect} # options ? {:only_path=>false}.merge(options) : {:only_path=>false} + end # end + protected :#{selector} # protected :hash_for_users_url + end_eval + helpers << selector + end + + def define_url_helper(route, name, kind, options) + selector = url_helper_name(name, kind) + # The segment keys used for positional parameters + + hash_access_method = hash_access_name(name, kind) + + # allow ordered parameters to be associated with corresponding + # dynamic segments, so you can do + # + # foo_url(bar, baz, bang) + # + # instead of + # + # foo_url(:bar => bar, :baz => baz, :bang => bang) + # + # Also allow options hash, so you can do + # + # foo_url(bar, baz, bang, :sort_by => 'baz') + # + named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks + def #{selector}(*args) # def users_url(*args) + # + opts = if args.empty? || Hash === args.first # opts = if args.empty? || Hash === args.first + args.first || {} # args.first || {} + else # else + options = args.extract_options! # options = args.extract_options! + args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)| # args = args.zip([]).inject({}) do |h, (v, k)| + h[k] = v # h[k] = v + h # h + end # end + options.merge(args) # options.merge(args) + end # end + # + url_for(#{hash_access_method}(opts)) # url_for(hash_for_users_url(opts)) + # + end # end + #Add an alias to support the now deprecated formatted_* URL. # #Add an alias to support the now deprecated formatted_* URL. + def formatted_#{selector}(*args) # def formatted_users_url(*args) + ActiveSupport::Deprecation.warn( # ActiveSupport::Deprecation.warn( + "formatted_#{selector}() has been deprecated. " + # "formatted_users_url() has been deprecated. " + + "Please pass format to the standard " + # "Please pass format to the standard " + + "#{selector} method instead.", caller) # "users_url method instead.", caller) + #{selector}(*args) # users_url(*args) + end # end + protected :#{selector} # protected :users_url + end_eval + helpers << selector + end + end + + attr_accessor :routes, :named_routes, :configuration_files + + def initialize + self.configuration_files = [] + + self.routes = [] + self.named_routes = NamedRouteCollection.new + + clear! + end + + def draw(&block) + clear! + Mapper.new(self).instance_exec(DeprecatedMapper.new(self), &block) + @set.add_route(NotFound) + install_helpers + @set.freeze + end + + def clear! + routes.clear + named_routes.clear + @set = ::Rack::Mount::RouteSet.new(:parameters_key => PARAMETERS_KEY) + end + + 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 + + def empty? + routes.empty? + end + + def add_configuration_file(path) + self.configuration_files << path + end + + # Deprecated accessor + def configuration_file=(path) + add_configuration_file(path) + end + + # Deprecated accessor + def configuration_file + configuration_files + end + + def load! + Routing.use_controllers!(nil) # Clear the controller cache so we may discover new ones + load_routes! + end + + # reload! will always force a reload whereas load checks the timestamp first + alias reload! load! + + 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 + + def load_routes! + if configuration_files.any? + configuration_files.each { |config| load(config) } + @routes_last_modified = routes_changed_at + else + draw do |map| + map.connect ":controller/:action/:id" + end + end + end + + 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 + + def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil) + route = Route.new(app, conditions, requirements, defaults, name) + @set.add_route(*route) + named_routes[name] = route if name + routes << route + route + end + + def options_as_params(options) + # 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? + + options_as_params = options.clone + options_as_params[:action] ||= 'index' if options[:controller] + options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action] + options_as_params + end + + def build_expiry(options, recall) + recall.inject({}) do |expiry, (key, recalled_value)| + expiry[key] = (options.key?(key) && options[key].to_param != recalled_value.to_param) + expiry + end + end + + # Generate the path indicated by the arguments, and return an array of + # the keys that were not used to generate it. + def extra_keys(options, recall={}) + generate_extras(options, recall).last + end + + def generate_extras(options, recall={}) + generate(options, recall, :generate_extras) + end + + def generate(options, recall = {}, method = :generate) + options, recall = options.dup, recall.dup + named_route = options.delete(:use_route) + + options = options_as_params(options) + expire_on = build_expiry(options, recall) + + recall[:action] ||= 'index' if options[:controller] || recall[:controller] + + if recall[:controller] && (!options.has_key?(:controller) || options[:controller] == recall[:controller]) + options[:controller] = recall.delete(:controller) + + if recall[:action] && (!options.has_key?(:action) || options[:action] == recall[:action]) + options[:action] = recall.delete(:action) + + if recall[:id] && (!options.has_key?(:id) || options[:id] == recall[:id]) + options[:id] = recall.delete(:id) + end + end + end + + options[:controller] = options[:controller].to_s if options[:controller] + + if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/ + old_parts = recall[:controller].split('/') + new_parts = options[:controller].split('/') + parts = old_parts[0..-(new_parts.length + 1)] + new_parts + options[:controller] = parts.join('/') + end + + options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/ + + merged = options.merge(recall) + if options.has_key?(:action) && options[:action].nil? + options.delete(:action) + recall[:action] = 'index' + end + recall[:action] = options.delete(:action) if options[:action] == 'index' + + path = _uri(named_route, options, recall) + if path && method == :generate_extras + uri = URI(path) + extras = uri.query ? + Rack::Utils.parse_nested_query(uri.query).keys.map { |k| k.to_sym } : + [] + [uri.path, extras] + elsif path + path + else + raise ActionController::RoutingError, "No route matches #{options.inspect}" + end + rescue Rack::Mount::RoutingError + raise ActionController::RoutingError, "No route matches #{options.inspect}" + end + + def call(env) + @set.call(env) + rescue ActionController::RoutingError => e + raise e if env['action_controller.rescue_error'] == false + + method, path = env['REQUEST_METHOD'].downcase.to_sym, env['PATH_INFO'] + + # Route was not recognized. Try to find out why (maybe wrong verb). + allows = HTTP_METHODS.select { |verb| + begin + recognize_path(path, {:method => verb}, false) + rescue ActionController::RoutingError + nil + end + } + + if !HTTP_METHODS.include?(method) + raise ActionController::NotImplemented.new(*allows) + elsif !allows.empty? + raise ActionController::MethodNotAllowed.new(*allows) + else + raise e + end + end + + 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 + + def recognize_path(path, environment = {}, rescue_error = true) + method = (environment[:method] || "GET").to_s.upcase + + begin + env = Rack::MockRequest.env_for(path, {:method => method}) + rescue URI::InvalidURIError => e + raise ActionController::RoutingError, e.message + end + + env['action_controller.recognize'] = true + env['action_controller.rescue_error'] = rescue_error + status, headers, body = call(env) + body + end + + # Subclasses and plugins may override this method to extract further attributes + # from the request, for use by route conditions and such. + def extract_request_environment(request) + { :method => request.method } + end + + private + def _uri(named_route, params, recall) + params = URISegment.wrap_values(params) + recall = URISegment.wrap_values(recall) + + unless result = @set.generate(:path_info, named_route, params, recall) + return + end + + uri, params = result + params.each do |k, v| + if v._value + params[k] = v._value + else + params.delete(k) + end + end + + uri << "?#{Rack::Mount::Utils.build_nested_query(params)}" if uri && params.any? + uri + end + + class URISegment < Struct.new(:_value, :_escape) + EXCLUDED = [:controller] + + def self.wrap_values(hash) + hash.inject({}) { |h, (k, v)| + h[k] = new(v, !EXCLUDED.include?(k.to_sym)) + h + } + end + + extend Forwardable + def_delegators :_value, :==, :eql?, :hash + + def to_param + @to_param ||= begin + if _value.is_a?(Array) + _value.map { |v| _escaped(v) }.join('/') + else + _escaped(_value) + end + end + end + alias_method :to_s, :to_param + + private + def _escaped(value) + v = value.respond_to?(:to_param) ? value.to_param : value + _escape ? Rack::Mount::Utils.escape_uri(v) : v.to_s + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 58ebe94a5b..40d6f97b2a 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -490,7 +490,7 @@ module ActionDispatch def self.app # DEPRECATE Rails application fallback # This should be set by the initializer - @@app || (defined?(Rails.application) && Rails.application.new) || nil + @@app || (defined?(Rails.application) && Rails.application) || nil end def self.app=(app) |