diff options
Diffstat (limited to 'actionpack/lib/action_dispatch/routing')
-rw-r--r-- | actionpack/lib/action_dispatch/routing/endpoint.rb | 10 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/inspector.rb | 234 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/mapper.rb | 1939 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/polymorphic_routes.rb | 325 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/redirection.rb | 191 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/route_set.rb | 787 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/routes_proxy.rb | 41 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/url_for.rb | 189 |
8 files changed, 3716 insertions, 0 deletions
diff --git a/actionpack/lib/action_dispatch/routing/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb new file mode 100644 index 0000000000..88aa13c3e8 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/endpoint.rb @@ -0,0 +1,10 @@ +module ActionDispatch + module Routing + class Endpoint # :nodoc: + def dispatcher?; false; end + def redirect?; false; end + def matches?(req); true; end + def app; self; end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb new file mode 100644 index 0000000000..ea3b2f419d --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -0,0 +1,234 @@ +require 'delegate' +require 'active_support/core_ext/string/strip' + +module ActionDispatch + module Routing + class RouteWrapper < SimpleDelegator + def endpoint + app.dispatcher? ? "#{controller}##{action}" : rack_app.inspect + end + + def constraints + requirements.except(:controller, :action) + end + + def rack_app + app.app + end + + def verb + super.source.gsub(/[$^]/, '') + end + + def path + super.spec.to_s + end + + def name + super.to_s + end + + def regexp + __getobj__.path.to_regexp + end + + def json_regexp + str = regexp.inspect. + sub('\\A' , '^'). + sub('\\Z' , '$'). + sub('\\z' , '$'). + sub(/^\// , ''). + sub(/\/[a-z]*$/ , ''). + gsub(/\(\?#.+\)/ , ''). + gsub(/\(\?-\w+:/ , '('). + gsub(/\s/ , '') + Regexp.new(str).source + end + + def reqs + @reqs ||= begin + reqs = endpoint + reqs += " #{constraints.to_s}" unless constraints.empty? + reqs + end + end + + def controller + requirements[:controller] || ':controller' + end + + def action + requirements[:action] || ':action' + end + + def internal? + controller.to_s =~ %r{\Arails/(info|mailers|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}\z} + end + + def engine? + rack_app.respond_to?(:routes) + end + end + + ## + # This class is just used for displaying route information when someone + # executes `rake routes` or looks at the RoutingError page. + # People should not use this class. + class RoutesInspector # :nodoc: + def initialize(routes) + @engines = {} + @routes = routes + end + + def format(formatter, filter = nil) + routes_to_display = filter_routes(filter) + + routes = collect_routes(routes_to_display) + + if routes.none? + formatter.no_routes + return formatter.result + end + + formatter.header routes + formatter.section routes + + @engines.each do |name, engine_routes| + formatter.section_title "Routes for #{name}" + formatter.section engine_routes + end + + formatter.result + end + + private + + def filter_routes(filter) + if filter + @routes.select { |route| route.defaults[:controller] == filter } + else + @routes + end + end + + def collect_routes(routes) + routes.collect do |route| + RouteWrapper.new(route) + end.reject do |route| + route.internal? + end.collect do |route| + collect_engine_routes(route) + + { name: route.name, + verb: route.verb, + path: route.path, + reqs: route.reqs, + regexp: route.json_regexp } + end + end + + def collect_engine_routes(route) + name = route.endpoint + return unless route.engine? + return if @engines[name] + + routes = route.rack_app.routes + if routes.is_a?(ActionDispatch::Routing::RouteSet) + @engines[name] = collect_routes(routes.routes) + end + end + end + + class ConsoleFormatter + def initialize + @buffer = [] + end + + def result + @buffer.join("\n") + end + + def section_title(title) + @buffer << "\n#{title}:" + end + + def section(routes) + @buffer << draw_section(routes) + end + + def header(routes) + @buffer << draw_header(routes) + end + + def no_routes + @buffer << <<-MESSAGE.strip_heredoc + You don't have any routes defined! + + Please add some routes in config/routes.rb. + + For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html. + MESSAGE + end + + private + def draw_section(routes) + header_lengths = ['Prefix', 'Verb', 'URI Pattern'].map(&:length) + name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max) + + routes.map do |r| + "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}" + end + end + + def draw_header(routes) + name_width, verb_width, path_width = widths(routes) + + "#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action" + end + + def widths(routes) + [routes.map { |r| r[:name].length }.max || 0, + routes.map { |r| r[:verb].length }.max || 0, + routes.map { |r| r[:path].length }.max || 0] + end + end + + class HtmlTableFormatter + def initialize(view) + @view = view + @buffer = [] + end + + def section_title(title) + @buffer << %(<tr><th colspan="4">#{title}</th></tr>) + end + + def section(routes) + @buffer << @view.render(partial: "routes/route", collection: routes) + end + + # the header is part of the HTML page, so we don't construct it here. + def header(routes) + end + + def no_routes + @buffer << <<-MESSAGE.strip_heredoc + <p>You don't have any routes defined!</p> + <ul> + <li>Please add some routes in <tt>config/routes.rb</tt>.</li> + <li> + For more information about routes, please see the Rails guide + <a href="http://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>. + </li> + </ul> + MESSAGE + end + + def result + @view.raw @view.render(layout: "routes/table") { + @view.raw @buffer.join("\n") + } + 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..cd94f35e8f --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -0,0 +1,1939 @@ +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/hash/reverse_merge' +require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/enumerable' +require 'active_support/core_ext/array/extract_options' +require 'active_support/core_ext/module/remove_method' +require 'active_support/inflector' +require 'action_dispatch/routing/redirection' +require 'action_dispatch/routing/endpoint' +require 'active_support/deprecation' + +module ActionDispatch + module Routing + class Mapper + URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port] + + class Constraints < Endpoint #:nodoc: + attr_reader :app, :constraints + + def initialize(app, constraints, dispatcher_p) + # Unwrap Constraints objects. I don't actually think it's possible + # to pass a Constraints object to this constructor, but there were + # multiple places that kept testing children of this object. I + # *think* they were just being defensive, but I have no idea. + if app.is_a?(self.class) + constraints += app.constraints + app = app.app + end + + @dispatcher = dispatcher_p + + @app, @constraints, = app, constraints + end + + def dispatcher?; @dispatcher; end + + def matches?(req) + @constraints.all? do |constraint| + (constraint.respond_to?(:matches?) && constraint.matches?(req)) || + (constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req))) + end + end + + def serve(req) + return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req) + + if dispatcher? + @app.serve req + else + @app.call req.env + end + end + + private + def constraint_args(constraint, request) + constraint.arity == 1 ? [request] : [request.path_parameters, request] + end + end + + class Mapping #:nodoc: + ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + + attr_reader :requirements, :conditions, :defaults + attr_reader :to, :default_controller, :default_action, :as, :anchor + + def self.build(scope, set, path, options) + options = scope[:options].merge(options) if scope[:options] + + options.delete :only + options.delete :except + options.delete :shallow_path + options.delete :shallow_prefix + options.delete :shallow + + defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {} + + new scope, set, path, defaults, options + end + + def initialize(scope, set, path, defaults, options) + @requirements, @conditions = {}, {} + @defaults = defaults + @set = set + + @to = options.delete :to + @default_controller = options.delete(:controller) || scope[:controller] + @default_action = options.delete(:action) || scope[:action] + @as = options.delete :as + @anchor = options.delete :anchor + + formatted = options.delete :format + via = Array(options.delete(:via) { [] }) + options_constraints = options.delete :constraints + + path = normalize_path! path, formatted + ast = path_ast path + path_params = path_params ast + + options = normalize_options!(options, formatted, path_params, ast, scope[:module]) + + + split_constraints(path_params, scope[:constraints]) if scope[:constraints] + constraints = constraints(options, path_params) + + split_constraints path_params, constraints + + @blocks = blocks(options_constraints, scope[:blocks]) + + if options_constraints.is_a?(Hash) + split_constraints path_params, options_constraints + options_constraints.each do |key, default| + if URL_OPTIONS.include?(key) && (String === default || Fixnum === default) + @defaults[key] ||= default + end + end + end + + normalize_format!(formatted) + + @conditions[:path_info] = path + @conditions[:parsed_path_info] = ast + + add_request_method(via, @conditions) + normalize_defaults!(options) + end + + def to_route + [ app(@blocks), conditions, requirements, defaults, as, anchor ] + end + + private + + def normalize_path!(path, format) + path = Mapper.normalize_path(path) + + if format == true + "#{path}.:format" + elsif optional_format?(path, format) + "#{path}(.:format)" + else + path + end + end + + def optional_format?(path, format) + format != false && !path.include?(':format') && !path.end_with?('/') + end + + def normalize_options!(options, formatted, path_params, path_ast, modyoule) + # Add a constraint for wildcard route to make it non-greedy and match the + # optional format part of the route by default + if formatted != false + path_ast.grep(Journey::Nodes::Star) do |node| + options[node.name.to_sym] ||= /.+?/ + end + end + + if path_params.include?(:controller) + raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule + + # Add a default constraint for :controller path segments that matches namespaced + # controllers with default routes like :controller/:action/:id(.:format), e.g: + # GET /admin/products/show/1 + # => { controller: 'admin/products', action: 'show', id: '1' } + options[:controller] ||= /.+?/ + end + + if to.respond_to? :call + options + else + to_endpoint = split_to to + controller = to_endpoint[0] || default_controller + action = to_endpoint[1] || default_action + + controller = add_controller_module(controller, modyoule) + + options.merge! check_controller_and_action(path_params, controller, action) + end + end + + def split_constraints(path_params, constraints) + constraints.each_pair do |key, requirement| + if path_params.include?(key) || key == :controller + verify_regexp_requirement(requirement) if requirement.is_a?(Regexp) + @requirements[key] = requirement + else + @conditions[key] = requirement + end + end + end + + def normalize_format!(formatted) + if formatted == true + @requirements[:format] ||= /.+/ + elsif Regexp === formatted + @requirements[:format] = formatted + @defaults[:format] = nil + elsif String === formatted + @requirements[:format] = Regexp.compile(formatted) + @defaults[:format] = formatted + end + end + + def verify_regexp_requirement(requirement) + if requirement.source =~ ANCHOR_CHARACTERS_REGEX + raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" + end + + if requirement.multiline? + raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}" + end + end + + def normalize_defaults!(options) + options.each_pair do |key, default| + unless Regexp === default + @defaults[key] = default + end + end + end + + def verify_callable_constraint(callable_constraint) + unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?) + raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?" + end + end + + def add_request_method(via, conditions) + return if via == [:all] + + if via.empty? + msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \ + "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \ + "If you want to expose your action to GET, use `get` in the router:\n" \ + " Instead of: match \"controller#action\"\n" \ + " Do: get \"controller#action\"" + raise ArgumentError, msg + end + + conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase } + end + + def app(blocks) + return to if Redirect === to + + if to.respond_to?(:call) + Constraints.new(to, blocks, false) + else + if blocks.any? + Constraints.new(dispatcher(defaults), blocks, true) + else + dispatcher(defaults) + end + end + end + + def check_controller_and_action(path_params, controller, action) + hash = check_part(:controller, controller, path_params, {}) do |part| + translate_controller(part) { + message = "'#{part}' is not a supported controller name. This can lead to potential routing problems." + message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use" + + raise ArgumentError, message + } + end + + check_part(:action, action, path_params, hash) { |part| + part.is_a?(Regexp) ? part : part.to_s + } + end + + def check_part(name, part, path_params, hash) + if part + hash[name] = yield(part) + else + unless path_params.include?(name) + message = "Missing :#{name} key on routes definition, please check your routes." + raise ArgumentError, message + end + end + hash + end + + def split_to(to) + case to + when Symbol + ActiveSupport::Deprecation.warn "defining a route where `to` is a symbol is deprecated. Please change \"to: :#{to}\" to \"action: :#{to}\"" + [nil, to.to_s] + when /#/ then to.split('#') + when String + ActiveSupport::Deprecation.warn "defining a route where `to` is a controller without an action is deprecated. Please change \"to: :#{to}\" to \"controller: :#{to}\"" + [to, nil] + else + [] + end + end + + def add_controller_module(controller, modyoule) + if modyoule && !controller.is_a?(Regexp) + if controller =~ %r{\A/} + controller[1..-1] + else + [modyoule, controller].compact.join("/") + end + else + controller + end + end + + def translate_controller(controller) + return controller if Regexp === controller + return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/ + + yield + end + + def blocks(options_constraints, scope_blocks) + if options_constraints && !options_constraints.is_a?(Hash) + verify_callable_constraint(options_constraints) + [options_constraints] + else + scope_blocks || [] + end + end + + def constraints(options, path_params) + constraints = {} + required_defaults = [] + options.each_pair do |key, option| + if Regexp === option + constraints[key] = option + else + required_defaults << key unless path_params.include?(key) + end + end + @conditions[:required_defaults] = required_defaults + constraints + end + + def path_params(ast) + ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym } + end + + def path_ast(path) + parser = Journey::Parser.new + parser.parse path + end + + def dispatcher(defaults) + @set.dispatcher defaults + end + end + + # Invokes Journey::Router::Utils.normalize_path and ensure that + # (:locale) becomes (/:locale) instead of /(:locale). Except + # for root cases, where the latter is the correct one. + def self.normalize_path(path) + path = Journey::Router::Utils.normalize_path(path) + path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$} + path + end + + def self.normalize_name(name) + normalize_path(name)[1..-1].tr("/", "_") + end + + module Base + # You can specify what Rails should route "/" to with the root method: + # + # root to: 'pages#main' + # + # For options, see +match+, as +root+ uses it internally. + # + # You can also pass a string which will expand + # + # root 'pages#main' + # + # You should put the root route at the top of <tt>config/routes.rb</tt>, + # because this means it will be matched first. As this is the most popular route + # of most Rails applications, this is beneficial. + def root(options = {}) + match '/', { :as => :root, :via => :get }.merge!(options) + end + + # Matches a url pattern to one or more routes. + # + # You should not use the `match` method in your router + # without specifying an HTTP method. + # + # If you want to expose your action to both GET and POST, use: + # + # # sets :controller, :action and :id in params + # match ':controller/:action/:id', via: [:get, :post] + # + # Note that +:controller+, +:action+ and +:id+ are interpreted as url + # query parameters and thus available through +params+ in an action. + # + # If you want to expose your action to GET, use `get` in the router: + # + # Instead of: + # + # match ":controller/:action/:id" + # + # Do: + # + # get ":controller/:action/:id" + # + # Two of these symbols are special, +:controller+ maps to the controller + # and +:action+ to the controller's action. A pattern can also map + # wildcard segments (globs) to params: + # + # get 'songs/*category/:title', to: 'songs#show' + # + # # 'songs/rock/classic/stairway-to-heaven' sets + # # params[:category] = 'rock/classic' + # # params[:title] = 'stairway-to-heaven' + # + # To match a wildcard parameter, it must have a name assigned to it. + # Without a variable name to attach the glob parameter to, the route + # can't be parsed. + # + # When a pattern points to an internal route, the route's +:action+ and + # +:controller+ should be set in options or hash shorthand. Examples: + # + # match 'photos/:id' => 'photos#show', via: :get + # match 'photos/:id', to: 'photos#show', via: :get + # match 'photos/:id', controller: 'photos', action: 'show', via: :get + # + # A pattern can also point to a +Rack+ endpoint i.e. anything that + # responds to +call+: + # + # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get + # match 'photos/:id', to: PhotoRackApp, via: :get + # # Yes, controller actions are just rack endpoints + # match 'photos/:id', to: PhotosController.action(:show), via: :get + # + # Because requesting various HTTP verbs with a single action has security + # implications, you must either specify the actions in + # the via options or use one of the HtttpHelpers[rdoc-ref:HttpHelpers] + # instead +match+ + # + # === Options + # + # Any options not seen here are passed on as params with the url. + # + # [:controller] + # The route's controller. + # + # [:action] + # The route's action. + # + # [:param] + # Overrides the default resource identifier `:id` (name of the + # dynamic segment used to generate the routes). + # You can access that segment from your controller using + # <tt>params[<:param>]</tt>. + # + # [:path] + # The path prefix for the routes. + # + # [:module] + # The namespace for :controller. + # + # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get + # # => Sekret::PostsController + # + # See <tt>Scoping#namespace</tt> for its scope equivalent. + # + # [:as] + # The name used to generate routing helpers. + # + # [:via] + # Allowed HTTP verb(s) for route. + # + # match 'path', to: 'c#a', via: :get + # match 'path', to: 'c#a', via: [:get, :post] + # match 'path', to: 'c#a', via: :all + # + # [:to] + # Points to a +Rack+ endpoint. Can be an object that responds to + # +call+ or a string representing a controller's action. + # + # match 'path', to: 'controller#action', via: :get + # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get + # match 'path', to: RackApp, via: :get + # + # [:on] + # Shorthand for wrapping routes in a specific RESTful context. Valid + # values are +:member+, +:collection+, and +:new+. Only use within + # <tt>resource(s)</tt> block. For example: + # + # resource :bar do + # match 'foo', to: 'c#a', on: :member, via: [:get, :post] + # end + # + # Is equivalent to: + # + # resource :bar do + # member do + # match 'foo', to: 'c#a', via: [:get, :post] + # end + # end + # + # [:constraints] + # Constrains parameters with a hash of regular expressions + # or an object that responds to <tt>matches?</tt>. In addition, constraints + # other than path can also be specified with any object + # that responds to <tt>===</tt> (eg. String, Array, Range, etc.). + # + # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get + # + # match 'json_only', constraints: { format: 'json' }, via: :get + # + # class Whitelist + # def matches?(request) request.remote_ip == '1.2.3.4' end + # end + # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get + # + # See <tt>Scoping#constraints</tt> for more examples with its scope + # equivalent. + # + # [:defaults] + # Sets defaults for parameters + # + # # Sets params[:format] to 'jpg' by default + # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get + # + # See <tt>Scoping#defaults</tt> for its scope equivalent. + # + # [:anchor] + # Boolean to anchor a <tt>match</tt> pattern. Default is true. When set to + # false, the pattern matches any request prefixed with the given path. + # + # # Matches any request starting with 'path' + # match 'path', to: 'c#a', anchor: false, via: :get + # + # [:format] + # Allows you to specify the default value for optional +format+ + # segment or disable it by supplying +false+. + def match(path, options=nil) + end + + # Mount a Rack-based application to be used within the application. + # + # mount SomeRackApp, at: "some_route" + # + # Alternatively: + # + # mount(SomeRackApp => "some_route") + # + # For options, see +match+, as +mount+ uses it internally. + # + # All mounted applications come with routing helpers to access them. + # These are named after the class specified, so for the above example + # the helper is either +some_rack_app_path+ or +some_rack_app_url+. + # To customize this helper's name, use the +:as+ option: + # + # mount(SomeRackApp => "some_route", as: "exciting") + # + # This will generate the +exciting_path+ and +exciting_url+ helpers + # which can be used to navigate to this mounted app. + def mount(app, options = nil) + if options + path = options.delete(:at) + else + unless Hash === app + raise ArgumentError, "must be called with mount point" + end + + options = app + app, path = options.find { |k, _| k.respond_to?(:call) } + options.delete(app) if app + end + + raise "A rack application must be specified" unless path + + rails_app = rails_app? app + + if rails_app + options[:as] ||= app.railtie_name + else + # non rails apps can't have an :as + options[:as] = nil + end + + target_as = name_for_action(options[:as], path) + options[:via] ||= :all + + match(path, options.merge(:to => app, :anchor => false, :format => false)) + + define_generate_prefix(app, target_as) if rails_app + self + end + + def default_url_options=(options) + @set.default_url_options = options + end + alias_method :default_url_options, :default_url_options= + + def with_default_scope(scope, &block) + scope(scope) do + instance_exec(&block) + end + end + + # Query if the following named route was already defined. + def has_named_route?(name) + @set.named_routes.routes[name.to_sym] + end + + private + def rails_app?(app) + app.is_a?(Class) && app < Rails::Railtie + end + + def define_generate_prefix(app, name) + _route = @set.named_routes.get name + _routes = @set + app.routes.define_mounted_helper(name) + app.routes.extend Module.new { + def optimize_routes_generation?; false; end + define_method :find_script_name do |options| + if options.key? :script_name + super(options) + else + prefix_options = options.slice(*_route.segment_keys) + # we must actually delete prefix segment keys to avoid passing them to next url_for + _route.segment_keys.each { |k| options.delete(k) } + _routes.url_helpers.send("#{name}_path", prefix_options) + end + end + } + end + end + + module HttpHelpers + # Define a route that only recognizes HTTP GET. + # For supported arguments, see match[rdoc-ref:Base#match] + # + # get 'bacon', to: 'food#bacon' + def get(*args, &block) + map_method(:get, args, &block) + end + + # Define a route that only recognizes HTTP POST. + # For supported arguments, see match[rdoc-ref:Base#match] + # + # post 'bacon', to: 'food#bacon' + def post(*args, &block) + map_method(:post, args, &block) + end + + # Define a route that only recognizes HTTP PATCH. + # For supported arguments, see match[rdoc-ref:Base#match] + # + # patch 'bacon', to: 'food#bacon' + def patch(*args, &block) + map_method(:patch, args, &block) + end + + # Define a route that only recognizes HTTP PUT. + # For supported arguments, see match[rdoc-ref:Base#match] + # + # put 'bacon', to: 'food#bacon' + def put(*args, &block) + map_method(:put, args, &block) + end + + # Define a route that only recognizes HTTP DELETE. + # For supported arguments, see match[rdoc-ref:Base#match] + # + # delete 'broccoli', to: 'food#broccoli' + def delete(*args, &block) + map_method(:delete, args, &block) + end + + private + def map_method(method, args, &block) + options = args.extract_options! + options[:via] = method + match(*args, options, &block) + self + end + end + + # You may wish to organize groups of controllers under a namespace. + # Most commonly, you might group a number of administrative controllers + # under an +admin+ namespace. You would place these controllers under + # the <tt>app/controllers/admin</tt> directory, and you can group them + # together in your router: + # + # namespace "admin" do + # resources :posts, :comments + # end + # + # This will create a number of routes for each of the posts and comments + # controller. For <tt>Admin::PostsController</tt>, Rails will create: + # + # GET /admin/posts + # GET /admin/posts/new + # POST /admin/posts + # GET /admin/posts/1 + # GET /admin/posts/1/edit + # PATCH/PUT /admin/posts/1 + # DELETE /admin/posts/1 + # + # If you want to route /posts (without the prefix /admin) to + # <tt>Admin::PostsController</tt>, you could use + # + # scope module: "admin" do + # resources :posts + # end + # + # or, for a single case + # + # resources :posts, module: "admin" + # + # If you want to route /admin/posts to +PostsController+ + # (without the <tt>Admin::</tt> module prefix), you could use + # + # scope "/admin" do + # resources :posts + # end + # + # or, for a single case + # + # resources :posts, path: "/admin/posts" + # + # In each of these cases, the named routes remain the same as if you did + # not use scope. In the last case, the following paths map to + # +PostsController+: + # + # GET /admin/posts + # GET /admin/posts/new + # POST /admin/posts + # GET /admin/posts/1 + # GET /admin/posts/1/edit + # PATCH/PUT /admin/posts/1 + # DELETE /admin/posts/1 + module Scoping + # Scopes a set of routes to the given default options. + # + # Take the following route definition as an example: + # + # scope path: ":account_id", as: "account" do + # resources :projects + # end + # + # This generates helpers such as +account_projects_path+, just like +resources+ does. + # The difference here being that the routes generated are like /:account_id/projects, + # rather than /accounts/:account_id/projects. + # + # === Options + # + # Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>. + # + # # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt> + # scope module: "admin" do + # resources :posts + # end + # + # # prefix the posts resource's requests with '/admin' + # scope path: "/admin" do + # resources :posts + # end + # + # # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+ + # scope as: "sekret" do + # resources :posts + # end + def scope(*args) + options = args.extract_options!.dup + scope = {} + + options[:path] = args.flatten.join('/') if args.any? + options[:constraints] ||= {} + + unless nested_scope? + options[:shallow_path] ||= options[:path] if options.key?(:path) + options[:shallow_prefix] ||= options[:as] if options.key?(:as) + end + + if options[:constraints].is_a?(Hash) + defaults = options[:constraints].select do + |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) + end + + (options[:defaults] ||= {}).reverse_merge!(defaults) + else + block, options[:constraints] = options[:constraints], {} + end + + @scope.options.each do |option| + if option == :blocks + value = block + elsif option == :options + value = options + else + value = options.delete(option) + end + + if value + scope[option] = send("merge_#{option}_scope", @scope[option], value) + end + end + + @scope = @scope.new scope + yield + self + ensure + @scope = @scope.parent + end + + # Scopes routes to a specific controller + # + # controller "food" do + # match "bacon", action: "bacon" + # end + def controller(controller, options={}) + options[:controller] = controller + scope(options) { yield } + end + + # Scopes routes to a specific namespace. For example: + # + # namespace :admin do + # resources :posts + # end + # + # This generates the following routes: + # + # admin_posts GET /admin/posts(.:format) admin/posts#index + # admin_posts POST /admin/posts(.:format) admin/posts#create + # new_admin_post GET /admin/posts/new(.:format) admin/posts#new + # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit + # admin_post GET /admin/posts/:id(.:format) admin/posts#show + # admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update + # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy + # + # === Options + # + # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+ + # options all default to the name of the namespace. + # + # For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see + # <tt>Resources#resources</tt>. + # + # # accessible through /sekret/posts rather than /admin/posts + # namespace :admin, path: "sekret" do + # resources :posts + # end + # + # # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt> + # namespace :admin, module: "sekret" do + # resources :posts + # end + # + # # generates +sekret_posts_path+ rather than +admin_posts_path+ + # namespace :admin, as: "sekret" do + # resources :posts + # end + def namespace(path, options = {}) + path = path.to_s + + defaults = { + module: path, + path: options.fetch(:path, path), + as: options.fetch(:as, path), + shallow_path: options.fetch(:path, path), + shallow_prefix: options.fetch(:as, path) + } + + scope(defaults.merge!(options)) { yield } + end + + # === Parameter Restriction + # Allows you to constrain the nested routes based on a set of rules. + # For instance, in order to change the routes to allow for a dot character in the +id+ parameter: + # + # constraints(id: /\d+\.\d+/) do + # resources :posts + # end + # + # Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be. + # The +id+ parameter must match the constraint passed in for this example. + # + # You may use this to also restrict other parameters: + # + # resources :posts do + # constraints(post_id: /\d+\.\d+/) do + # resources :comments + # end + # end + # + # === Restricting based on IP + # + # Routes can also be constrained to an IP or a certain range of IP addresses: + # + # constraints(ip: /192\.168\.\d+\.\d+/) do + # resources :posts + # end + # + # Any user connecting from the 192.168.* range will be able to see this resource, + # where as any user connecting outside of this range will be told there is no such route. + # + # === Dynamic request matching + # + # Requests to routes can be constrained based on specific criteria: + # + # constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do + # resources :iphones + # end + # + # You are able to move this logic out into a class if it is too complex for routes. + # This class must have a +matches?+ method defined on it which either returns +true+ + # if the user should be given access to that route, or +false+ if the user should not. + # + # class Iphone + # def self.matches?(request) + # request.env["HTTP_USER_AGENT"] =~ /iPhone/ + # end + # end + # + # An expected place for this code would be +lib/constraints+. + # + # This class is then used like this: + # + # constraints(Iphone) do + # resources :iphones + # end + def constraints(constraints = {}) + scope(:constraints => constraints) { yield } + end + + # Allows you to set default parameters for a route, such as this: + # defaults id: 'home' do + # match 'scoped_pages/(:id)', to: 'pages#show' + # end + # Using this, the +:id+ parameter here will default to 'home'. + def defaults(defaults = {}) + scope(:defaults => defaults) { yield } + end + + private + def merge_path_scope(parent, child) #:nodoc: + Mapper.normalize_path("#{parent}/#{child}") + end + + def merge_shallow_path_scope(parent, child) #:nodoc: + Mapper.normalize_path("#{parent}/#{child}") + end + + def merge_as_scope(parent, child) #:nodoc: + parent ? "#{parent}_#{child}" : child + end + + def merge_shallow_prefix_scope(parent, child) #:nodoc: + parent ? "#{parent}_#{child}" : child + end + + def merge_module_scope(parent, child) #:nodoc: + parent ? "#{parent}/#{child}" : child + end + + def merge_controller_scope(parent, child) #:nodoc: + child + end + + def merge_action_scope(parent, child) #:nodoc: + child + end + + def merge_path_names_scope(parent, child) #:nodoc: + merge_options_scope(parent, child) + end + + def merge_constraints_scope(parent, child) #:nodoc: + merge_options_scope(parent, child) + end + + def merge_defaults_scope(parent, child) #:nodoc: + merge_options_scope(parent, child) + end + + def merge_blocks_scope(parent, child) #:nodoc: + merged = parent ? parent.dup : [] + merged << child if child + merged + end + + def merge_options_scope(parent, child) #:nodoc: + (parent || {}).except(*override_keys(child)).merge!(child) + end + + def merge_shallow_scope(parent, child) #:nodoc: + child ? true : false + end + + def override_keys(child) #:nodoc: + child.key?(:only) || child.key?(:except) ? [:only, :except] : [] + end + end + + # Resource routing allows you to quickly declare all of the common routes + # for a given resourceful controller. Instead of declaring separate routes + # for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+ + # actions, a resourceful route declares them in a single line of code: + # + # resources :photos + # + # Sometimes, you have a resource that clients always look up without + # referencing an ID. A common example, /profile always shows the profile of + # the currently logged in user. In this case, you can use a singular resource + # to map /profile (rather than /profile/:id) to the show action. + # + # resource :profile + # + # It's common to have resources that are logically children of other + # resources: + # + # resources :magazines do + # resources :ads + # end + # + # You may wish to organize groups of controllers under a namespace. Most + # commonly, you might group a number of administrative controllers under + # an +admin+ namespace. You would place these controllers under the + # <tt>app/controllers/admin</tt> directory, and you can group them together + # in your router: + # + # namespace "admin" do + # resources :posts, :comments + # end + # + # By default the +:id+ parameter doesn't accept dots. If you need to + # use dots as part of the +:id+ parameter add a constraint which + # overrides this restriction, e.g: + # + # resources :articles, id: /[^\/]+/ + # + # This allows any character other than a slash as part of your +:id+. + # + module Resources + # CANONICAL_ACTIONS holds all actions that does not need a prefix or + # a path appended since they fit properly in their scope level. + VALID_ON_OPTIONS = [:new, :collection, :member] + RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns] + CANONICAL_ACTIONS = %w(index create new show update destroy) + RESOURCE_METHOD_SCOPES = [:collection, :member, :new] + RESOURCE_SCOPES = [:resource, :resources] + + class Resource #:nodoc: + attr_reader :controller, :path, :options, :param + + def initialize(entities, options = {}) + @name = entities.to_s + @path = (options[:path] || @name).to_s + @controller = (options[:controller] || @name).to_s + @as = options[:as] + @param = (options[:param] || :id).to_sym + @options = options + @shallow = false + end + + def default_actions + [:index, :create, :new, :show, :update, :destroy, :edit] + end + + def actions + if only = @options[:only] + Array(only).map(&:to_sym) + elsif except = @options[:except] + default_actions - Array(except).map(&:to_sym) + else + default_actions + end + end + + def name + @as || @name + end + + def plural + @plural ||= name.to_s + end + + def singular + @singular ||= name.to_s.singularize + end + + alias :member_name :singular + + # Checks for uncountable plurals, and appends "_index" if the plural + # and singular form are the same. + def collection_name + singular == plural ? "#{plural}_index" : plural + end + + def resource_scope + { :controller => controller } + end + + alias :collection_scope :path + + def member_scope + "#{path}/:#{param}" + end + + alias :shallow_scope :member_scope + + def new_scope(new_path) + "#{path}/#{new_path}" + end + + def nested_param + :"#{singular}_#{param}" + end + + def nested_scope + "#{path}/:#{nested_param}" + end + + def shallow=(value) + @shallow = value + end + + def shallow? + @shallow + end + end + + class SingletonResource < Resource #:nodoc: + def initialize(entities, options) + super + @as = nil + @controller = (options[:controller] || plural).to_s + @as = options[:as] + end + + def default_actions + [:show, :create, :update, :destroy, :new, :edit] + end + + def plural + @plural ||= name.to_s.pluralize + end + + def singular + @singular ||= name.to_s + end + + alias :member_name :singular + alias :collection_name :singular + + alias :member_scope :path + alias :nested_scope :path + end + + def resources_path_names(options) + @scope[:path_names].merge!(options) + end + + # Sometimes, you have a resource that clients always look up without + # referencing an ID. A common example, /profile always shows the + # profile of the currently logged in user. In this case, you can use + # a singular resource to map /profile (rather than /profile/:id) to + # the show action: + # + # resource :profile + # + # creates six different routes in your application, all mapping to + # the +Profiles+ controller (note that the controller is named after + # the plural): + # + # GET /profile/new + # POST /profile + # GET /profile + # GET /profile/edit + # PATCH/PUT /profile + # DELETE /profile + # + # === Options + # Takes same options as +resources+. + def resource(*resources, &block) + options = resources.extract_options!.dup + + if apply_common_behavior_for(:resource, resources, options, &block) + return self + end + + resource_scope(:resource, SingletonResource.new(resources.pop, options)) do + yield if block_given? + + concerns(options[:concerns]) if options[:concerns] + + collection do + post :create + end if parent_resource.actions.include?(:create) + + new do + get :new + end if parent_resource.actions.include?(:new) + + set_member_mappings_for_resource + end + + self + end + + # In Rails, a resourceful route provides a mapping between HTTP verbs + # and URLs and controller actions. By convention, each action also maps + # to particular CRUD operations in a database. A single entry in the + # routing file, such as + # + # resources :photos + # + # creates seven different routes in your application, all mapping to + # the +Photos+ controller: + # + # GET /photos + # GET /photos/new + # POST /photos + # GET /photos/:id + # GET /photos/:id/edit + # PATCH/PUT /photos/:id + # DELETE /photos/:id + # + # Resources can also be nested infinitely by using this block syntax: + # + # resources :photos do + # resources :comments + # end + # + # This generates the following comments routes: + # + # GET /photos/:photo_id/comments + # GET /photos/:photo_id/comments/new + # POST /photos/:photo_id/comments + # GET /photos/:photo_id/comments/:id + # GET /photos/:photo_id/comments/:id/edit + # PATCH/PUT /photos/:photo_id/comments/:id + # DELETE /photos/:photo_id/comments/:id + # + # === Options + # Takes same options as <tt>Base#match</tt> as well as: + # + # [:path_names] + # Allows you to change the segment component of the +edit+ and +new+ actions. + # Actions not specified are not changed. + # + # resources :posts, path_names: { new: "brand_new" } + # + # The above example will now change /posts/new to /posts/brand_new + # + # [:path] + # Allows you to change the path prefix for the resource. + # + # resources :posts, path: 'postings' + # + # The resource and all segments will now route to /postings instead of /posts + # + # [:only] + # Only generate routes for the given actions. + # + # resources :cows, only: :show + # resources :cows, only: [:show, :index] + # + # [:except] + # Generate all routes except for the given actions. + # + # resources :cows, except: :show + # resources :cows, except: [:show, :index] + # + # [:shallow] + # Generates shallow routes for nested resource(s). When placed on a parent resource, + # generates shallow routes for all nested resources. + # + # resources :posts, shallow: true do + # resources :comments + # end + # + # Is the same as: + # + # resources :posts do + # resources :comments, except: [:show, :edit, :update, :destroy] + # end + # resources :comments, only: [:show, :edit, :update, :destroy] + # + # This allows URLs for resources that otherwise would be deeply nested such + # as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt> + # to be shortened to just <tt>/comments/1234</tt>. + # + # [:shallow_path] + # Prefixes nested shallow routes with the specified path. + # + # scope shallow_path: "sekret" do + # resources :posts do + # resources :comments, shallow: true + # end + # end + # + # The +comments+ resource here will have the following routes generated for it: + # + # post_comments GET /posts/:post_id/comments(.:format) + # post_comments POST /posts/:post_id/comments(.:format) + # new_post_comment GET /posts/:post_id/comments/new(.:format) + # edit_comment GET /sekret/comments/:id/edit(.:format) + # comment GET /sekret/comments/:id(.:format) + # comment PATCH/PUT /sekret/comments/:id(.:format) + # comment DELETE /sekret/comments/:id(.:format) + # + # [:shallow_prefix] + # Prefixes nested shallow route names with specified prefix. + # + # scope shallow_prefix: "sekret" do + # resources :posts do + # resources :comments, shallow: true + # end + # end + # + # The +comments+ resource here will have the following routes generated for it: + # + # post_comments GET /posts/:post_id/comments(.:format) + # post_comments POST /posts/:post_id/comments(.:format) + # new_post_comment GET /posts/:post_id/comments/new(.:format) + # edit_sekret_comment GET /comments/:id/edit(.:format) + # sekret_comment GET /comments/:id(.:format) + # sekret_comment PATCH/PUT /comments/:id(.:format) + # sekret_comment DELETE /comments/:id(.:format) + # + # [:format] + # Allows you to specify the default value for optional +format+ + # segment or disable it by supplying +false+. + # + # === Examples + # + # # routes call <tt>Admin::PostsController</tt> + # resources :posts, module: "admin" + # + # # resource actions are at /admin/posts. + # resources :posts, path: "admin/posts" + def resources(*resources, &block) + options = resources.extract_options!.dup + + if apply_common_behavior_for(:resources, resources, options, &block) + return self + end + + resource_scope(:resources, Resource.new(resources.pop, options)) do + yield if block_given? + + concerns(options[:concerns]) if options[:concerns] + + collection do + get :index if parent_resource.actions.include?(:index) + post :create if parent_resource.actions.include?(:create) + end + + new do + get :new + end if parent_resource.actions.include?(:new) + + set_member_mappings_for_resource + end + + self + end + + # To add a route to the collection: + # + # resources :photos do + # collection do + # get 'search' + # end + # end + # + # This will enable Rails to recognize paths such as <tt>/photos/search</tt> + # with GET, and route to the search action of +PhotosController+. It will also + # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt> + # route helpers. + def collection + unless resource_scope? + raise ArgumentError, "can't use collection outside resource(s) scope" + end + + with_scope_level(:collection) do + scope(parent_resource.collection_scope) do + yield + end + end + end + + # To add a member route, add a member block into the resource block: + # + # resources :photos do + # member do + # get 'preview' + # end + # end + # + # This will recognize <tt>/photos/1/preview</tt> with GET, and route to the + # preview action of +PhotosController+. It will also create the + # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers. + def member + unless resource_scope? + raise ArgumentError, "can't use member outside resource(s) scope" + end + + with_scope_level(:member) do + if shallow? + shallow_scope(parent_resource.member_scope) { yield } + else + scope(parent_resource.member_scope) { yield } + end + end + end + + def new + unless resource_scope? + raise ArgumentError, "can't use new outside resource(s) scope" + end + + with_scope_level(:new) do + scope(parent_resource.new_scope(action_path(:new))) do + yield + end + end + end + + def nested + unless resource_scope? + raise ArgumentError, "can't use nested outside resource(s) scope" + end + + with_scope_level(:nested) do + if shallow? && shallow_nesting_depth >= 1 + shallow_scope(parent_resource.nested_scope, nested_options) { yield } + else + scope(parent_resource.nested_scope, nested_options) { yield } + end + end + end + + # See ActionDispatch::Routing::Mapper::Scoping#namespace + def namespace(path, options = {}) + if resource_scope? + nested { super } + else + super + end + end + + def shallow + scope(:shallow => true) do + yield + end + end + + def shallow? + parent_resource.instance_of?(Resource) && @scope[:shallow] + end + + # match 'path' => 'controller#action' + # match 'path', to: 'controller#action' + # match 'path', 'otherpath', on: :member, via: :get + def match(path, *rest) + if rest.empty? && Hash === path + options = path + path, to = options.find { |name, _value| name.is_a?(String) } + + case to + when Symbol + options[:action] = to + when String + if to =~ /#/ + options[:to] = to + else + options[:controller] = to + end + else + options[:to] = to + end + + options.delete(path) + paths = [path] + else + options = rest.pop || {} + paths = [path] + rest + end + + options[:anchor] = true unless options.key?(:anchor) + + if options[:on] && !VALID_ON_OPTIONS.include?(options[:on]) + raise ArgumentError, "Unknown scope #{on.inspect} given to :on" + end + + if @scope[:controller] && @scope[:action] + options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}" + end + + paths.each do |_path| + route_options = options.dup + route_options[:path] ||= _path if _path.is_a?(String) + + path_without_format = _path.to_s.sub(/\(\.:format\)$/, '') + if using_match_shorthand?(path_without_format, route_options) + route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1') + route_options[:to].tr!("-", "_") + end + + decomposed_match(_path, route_options) + end + self + end + + def using_match_shorthand?(path, options) + path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$} + end + + def decomposed_match(path, options) # :nodoc: + if on = options.delete(:on) + send(on) { decomposed_match(path, options) } + else + case @scope[:scope_level] + when :resources + nested { decomposed_match(path, options) } + when :resource + member { decomposed_match(path, options) } + else + add_route(path, options) + end + end + end + + def add_route(action, options) # :nodoc: + path = path_for_action(action, options.delete(:path)) + raise ArgumentError, "path is required" if path.blank? + + action = action.to_s.dup + + if action =~ /^[\w\-\/]+$/ + options[:action] ||= action.tr('-', '_') unless action.include?("/") + else + action = nil + end + + if !options.fetch(:as, true) # if it's set to nil or false + options.delete(:as) + else + options[:as] = name_for_action(options[:as], action) + end + + mapping = Mapping.build(@scope, @set, URI.parser.escape(path), options) + app, conditions, requirements, defaults, as, anchor = mapping.to_route + @set.add_route(app, conditions, requirements, defaults, as, anchor) + end + + def root(path, options={}) + if path.is_a?(String) + options[:to] = path + elsif path.is_a?(Hash) and options.empty? + options = path + else + raise ArgumentError, "must be called with a path and/or options" + end + + if @scope[:scope_level] == :resources + with_scope_level(:root) do + scope(parent_resource.path) do + super(options) + end + end + else + super(options) + end + end + + protected + + def parent_resource #:nodoc: + @scope[:scope_level_resource] + end + + def apply_common_behavior_for(method, resources, options, &block) #:nodoc: + if resources.length > 1 + resources.each { |r| send(method, r, options, &block) } + return true + end + + if options.delete(:shallow) + shallow do + send(method, resources.pop, options, &block) + end + return true + end + + if resource_scope? + nested { send(method, resources.pop, options, &block) } + return true + end + + options.keys.each do |k| + (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp) + end + + scope_options = options.slice!(*RESOURCE_OPTIONS) + unless scope_options.empty? + scope(scope_options) do + send(method, resources.pop, options, &block) + end + return true + end + + unless action_options?(options) + options.merge!(scope_action_options) if scope_action_options? + end + + false + end + + def action_options?(options) #:nodoc: + options[:only] || options[:except] + end + + def scope_action_options? #:nodoc: + @scope[:options] && (@scope[:options][:only] || @scope[:options][:except]) + end + + def scope_action_options #:nodoc: + @scope[:options].slice(:only, :except) + end + + def resource_scope? #:nodoc: + RESOURCE_SCOPES.include? @scope[:scope_level] + end + + def resource_method_scope? #:nodoc: + RESOURCE_METHOD_SCOPES.include? @scope[:scope_level] + end + + def nested_scope? #:nodoc: + @scope[:scope_level] == :nested + end + + def with_exclusive_scope + begin + @scope = @scope.new(:as => nil, :path => nil) + + with_scope_level(:exclusive) do + yield + end + ensure + @scope = @scope.parent + end + end + + def with_scope_level(kind) + @scope = @scope.new(:scope_level => kind) + yield + ensure + @scope = @scope.parent + end + + def resource_scope(kind, resource) #:nodoc: + resource.shallow = @scope[:shallow] + @scope = @scope.new(:scope_level_resource => resource) + @nesting.push(resource) + + with_scope_level(kind) do + scope(parent_resource.resource_scope) { yield } + end + ensure + @nesting.pop + @scope = @scope.parent + end + + def nested_options #:nodoc: + options = { :as => parent_resource.member_name } + options[:constraints] = { + parent_resource.nested_param => param_constraint + } if param_constraint? + + options + end + + def nesting_depth #:nodoc: + @nesting.size + end + + def shallow_nesting_depth #:nodoc: + @nesting.select(&:shallow?).size + end + + def param_constraint? #:nodoc: + @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp) + end + + def param_constraint #:nodoc: + @scope[:constraints][parent_resource.param] + end + + def canonical_action?(action, flag) #:nodoc: + flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) + end + + def shallow_scope(path, options = {}) #:nodoc: + scope = { :as => @scope[:shallow_prefix], + :path => @scope[:shallow_path] } + @scope = @scope.new scope + + scope(path, options) { yield } + ensure + @scope = @scope.parent + end + + def path_for_action(action, path) #:nodoc: + if canonical_action?(action, path.blank?) + @scope[:path].to_s + else + "#{@scope[:path]}/#{action_path(action, path)}" + end + end + + def action_path(name, path = nil) #:nodoc: + name = name.to_sym if name.is_a?(String) + path || @scope[:path_names][name] || name.to_s + end + + def prefix_name_for_action(as, action) #:nodoc: + if as + prefix = as + elsif !canonical_action?(action, @scope[:scope_level]) + prefix = action + end + prefix.to_s.tr('-', '_') if prefix + end + + def name_for_action(as, action) #:nodoc: + prefix = prefix_name_for_action(as, action) + prefix = Mapper.normalize_name(prefix) if prefix + name_prefix = @scope[:as] + + if parent_resource + return nil unless as || action + + collection_name = parent_resource.collection_name + member_name = parent_resource.member_name + end + + name = case @scope[:scope_level] + when :nested + [name_prefix, prefix] + when :collection + [prefix, name_prefix, collection_name] + when :new + [prefix, :new, name_prefix, member_name] + when :member + [prefix, name_prefix, member_name] + when :root + [name_prefix, collection_name, prefix] + else + [name_prefix, member_name, prefix] + end + + if candidate = name.select(&:present?).join("_").presence + # If a name was not explicitly given, we check if it is valid + # and return nil in case it isn't. Otherwise, we pass the invalid name + # forward so the underlying router engine treats it and raises an exception. + if as.nil? + candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate) + else + candidate + end + end + end + + def set_member_mappings_for_resource + member do + get :edit if parent_resource.actions.include?(:edit) + get :show if parent_resource.actions.include?(:show) + if parent_resource.actions.include?(:update) + patch :update + put :update + end + delete :destroy if parent_resource.actions.include?(:destroy) + end + end + end + + # Routing Concerns allow you to declare common routes that can be reused + # inside others resources and routes. + # + # concern :commentable do + # resources :comments + # end + # + # concern :image_attachable do + # resources :images, only: :index + # end + # + # These concerns are used in Resources routing: + # + # resources :messages, concerns: [:commentable, :image_attachable] + # + # or in a scope or namespace: + # + # namespace :posts do + # concerns :commentable + # end + module Concerns + # Define a routing concern using a name. + # + # Concerns may be defined inline, using a block, or handled by + # another object, by passing that object as the second parameter. + # + # The concern object, if supplied, should respond to <tt>call</tt>, + # which will receive two parameters: + # + # * The current mapper + # * A hash of options which the concern object may use + # + # Options may also be used by concerns defined in a block by accepting + # a block parameter. So, using a block, you might do something as + # simple as limit the actions available on certain resources, passing + # standard resource options through the concern: + # + # concern :commentable do |options| + # resources :comments, options + # end + # + # resources :posts, concerns: :commentable + # resources :archived_posts do + # # Don't allow comments on archived posts + # concerns :commentable, only: [:index, :show] + # end + # + # Or, using a callable object, you might implement something more + # specific to your application, which would be out of place in your + # routes file. + # + # # purchasable.rb + # class Purchasable + # def initialize(defaults = {}) + # @defaults = defaults + # end + # + # def call(mapper, options = {}) + # options = @defaults.merge(options) + # mapper.resources :purchases + # mapper.resources :receipts + # mapper.resources :returns if options[:returnable] + # end + # end + # + # # routes.rb + # concern :purchasable, Purchasable.new(returnable: true) + # + # resources :toys, concerns: :purchasable + # resources :electronics, concerns: :purchasable + # resources :pets do + # concerns :purchasable, returnable: false + # end + # + # Any routing helpers can be used inside a concern. If using a + # callable, they're accessible from the Mapper that's passed to + # <tt>call</tt>. + def concern(name, callable = nil, &block) + callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) } + @concerns[name] = callable + end + + # Use the named concerns + # + # resources :posts do + # concerns :commentable + # end + # + # concerns also work in any routes helper that you want to use: + # + # namespace :posts do + # concerns :commentable + # end + def concerns(*args) + options = args.extract_options! + args.flatten.each do |name| + if concern = @concerns[name] + concern.call(self, options) + else + raise ArgumentError, "No concern named #{name} was found!" + end + end + end + end + + class Scope # :nodoc: + OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, + :controller, :action, :path_names, :constraints, + :shallow, :blocks, :defaults, :options] + + attr_reader :parent + + def initialize(hash, parent = {}) + @hash = hash + @parent = parent + end + + def options + OPTIONS + end + + def new(hash) + self.class.new hash, self + end + + def [](key) + @hash.fetch(key) { @parent[key] } + end + + def []=(k,v) + @hash[k] = v + end + end + + def initialize(set) #:nodoc: + @set = set + @scope = Scope.new({ :path_names => @set.resources_path_names }) + @concerns = {} + @nesting = [] + end + + include Base + include HttpHelpers + include Redirection + include Scoping + include Concerns + include Resources + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb new file mode 100644 index 0000000000..bd3696cda1 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -0,0 +1,325 @@ +require 'action_controller/model_naming' + +module ActionDispatch + module Routing + # Polymorphic URL helpers are methods for smart resolution to a named route call when + # given an Active Record model instance. They are to be used in combination with + # ActionController::Resources. + # + # These methods are useful when you want to generate correct URL or path to a RESTful + # resource without having to know the exact type of the record in question. + # + # Nested resources and/or namespaces are also supported, as illustrated in the example: + # + # polymorphic_url([:admin, @article, @comment]) + # + # results in: + # + # admin_article_comment_url(@article, @comment) + # + # == Usage within the framework + # + # Polymorphic URL helpers are used in a number of places throughout the \Rails framework: + # + # * <tt>url_for</tt>, so you can use it with a record as the argument, e.g. + # <tt>url_for(@article)</tt>; + # * ActionView::Helpers::FormHelper uses <tt>polymorphic_path</tt>, so you can write + # <tt>form_for(@article)</tt> without having to specify <tt>:url</tt> parameter for the form + # action; + # * <tt>redirect_to</tt> (which, in fact, uses <tt>url_for</tt>) so you can write + # <tt>redirect_to(post)</tt> in your controllers; + # * ActionView::Helpers::AtomFeedHelper, so you don't have to explicitly specify URLs + # for feed entries. + # + # == Prefixed polymorphic helpers + # + # In addition to <tt>polymorphic_url</tt> and <tt>polymorphic_path</tt> methods, a + # number of prefixed helpers are available as a shorthand to <tt>action: "..."</tt> + # in options. Those are: + # + # * <tt>edit_polymorphic_url</tt>, <tt>edit_polymorphic_path</tt> + # * <tt>new_polymorphic_url</tt>, <tt>new_polymorphic_path</tt> + # + # Example usage: + # + # edit_polymorphic_path(@post) # => "/posts/1/edit" + # polymorphic_path(@post, format: :pdf) # => "/posts/1.pdf" + # + # == Usage with mounted engines + # + # If you are using a mounted engine and you need to use a polymorphic_url + # pointing at the engine's routes, pass in the engine's route proxy as the first + # argument to the method. For example: + # + # polymorphic_url([blog, @post]) # calls blog.post_path(@post) + # form_for([blog, @post]) # => "/blog/posts/1" + # + module PolymorphicRoutes + include ActionController::ModelNaming + + # Constructs a call to a named RESTful route for the given record and returns the + # resulting URL string. For example: + # + # # calls post_url(post) + # polymorphic_url(post) # => "http://example.com/posts/1" + # polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1" + # polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1" + # polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1" + # polymorphic_url(Comment) # => "http://example.com/comments" + # + # ==== Options + # + # * <tt>:action</tt> - Specifies the action prefix for the named route: + # <tt>:new</tt> or <tt>:edit</tt>. Default is no prefix. + # * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>. + # Default is <tt>:url</tt>. + # + # Also includes all the options from <tt>url_for</tt>. These include such + # things as <tt>:anchor</tt> or <tt>:trailing_slash</tt>. Example usage + # is given below: + # + # polymorphic_url([blog, post], anchor: 'my_anchor') + # # => "http://example.com/blogs/1/posts/1#my_anchor" + # polymorphic_url([blog, post], anchor: 'my_anchor', script_name: "/my_app") + # # => "http://example.com/my_app/blogs/1/posts/1#my_anchor" + # + # For all of these options, see the documentation for <tt>url_for</tt>. + # + # ==== Functionality + # + # # an Article record + # polymorphic_url(record) # same as article_url(record) + # + # # a Comment record + # polymorphic_url(record) # same as comment_url(record) + # + # # it recognizes new records and maps to the collection + # record = Comment.new + # polymorphic_url(record) # same as comments_url() + # + # # the class of a record will also map to the collection + # polymorphic_url(Comment) # same as comments_url() + # + def polymorphic_url(record_or_hash_or_array, options = {}) + if Hash === record_or_hash_or_array + options = record_or_hash_or_array.merge(options) + record = options.delete :id + return polymorphic_url record, options + end + + opts = options.dup + action = opts.delete :action + type = opts.delete(:routing_type) || :url + + HelperMethodBuilder.polymorphic_method self, + record_or_hash_or_array, + action, + type, + opts + + end + + # Returns the path component of a URL for the given record. It uses + # <tt>polymorphic_url</tt> with <tt>routing_type: :path</tt>. + def polymorphic_path(record_or_hash_or_array, options = {}) + if Hash === record_or_hash_or_array + options = record_or_hash_or_array.merge(options) + record = options.delete :id + return polymorphic_path record, options + end + + opts = options.dup + action = opts.delete :action + type = :path + + HelperMethodBuilder.polymorphic_method self, + record_or_hash_or_array, + action, + type, + opts + end + + + %w(edit new).each do |action| + module_eval <<-EOT, __FILE__, __LINE__ + 1 + def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {}) + polymorphic_url( # polymorphic_url( + record_or_hash, # record_or_hash, + options.merge(:action => "#{action}")) # options.merge(:action => "edit")) + end # end + # + def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {}) + polymorphic_url( # polymorphic_url( + record_or_hash, # record_or_hash, + options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path)) + end # end + EOT + end + + private + + class HelperMethodBuilder # :nodoc: + CACHE = { 'path' => {}, 'url' => {} } + + def self.get(action, type) + type = type.to_s + CACHE[type].fetch(action) { build action, type } + end + + def self.url; CACHE['url'.freeze][nil]; end + def self.path; CACHE['path'.freeze][nil]; end + + def self.build(action, type) + prefix = action ? "#{action}_" : "" + suffix = type + if action.to_s == 'new' + HelperMethodBuilder.singular prefix, suffix + else + HelperMethodBuilder.plural prefix, suffix + end + end + + def self.singular(prefix, suffix) + new(->(name) { name.singular_route_key }, prefix, suffix) + end + + def self.plural(prefix, suffix) + new(->(name) { name.route_key }, prefix, suffix) + end + + def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options) + builder = get action, type + + case record_or_hash_or_array + when Array + if record_or_hash_or_array.empty? || record_or_hash_or_array.include?(nil) + raise ArgumentError, "Nil location provided. Can't build URI." + end + if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy) + recipient = record_or_hash_or_array.shift + end + + method, args = builder.handle_list record_or_hash_or_array + when String, Symbol + method, args = builder.handle_string record_or_hash_or_array + when Class + method, args = builder.handle_class record_or_hash_or_array + + when nil + raise ArgumentError, "Nil location provided. Can't build URI." + else + method, args = builder.handle_model record_or_hash_or_array + end + + + if options.empty? + recipient.send(method, *args) + else + recipient.send(method, *args, options) + end + end + + attr_reader :suffix, :prefix + + def initialize(key_strategy, prefix, suffix) + @key_strategy = key_strategy + @prefix = prefix + @suffix = suffix + end + + def handle_string(record) + [get_method_for_string(record), []] + end + + def handle_string_call(target, str) + target.send get_method_for_string str + end + + def handle_class(klass) + [get_method_for_class(klass), []] + end + + def handle_class_call(target, klass) + target.send get_method_for_class klass + end + + def handle_model(record) + args = [] + + model = record.to_model + name = if record.persisted? + args << model + model.class.model_name.singular_route_key + else + @key_strategy.call model.class.model_name + end + + named_route = prefix + "#{name}_#{suffix}" + + [named_route, args] + end + + def handle_model_call(target, model) + method, args = handle_model model + target.send(method, *args) + end + + def handle_list(list) + record_list = list.dup + record = record_list.pop + + args = [] + + route = record_list.map { |parent| + case parent + when Symbol, String + parent.to_s + when Class + args << parent + parent.model_name.singular_route_key + else + args << parent.to_model + parent.to_model.class.model_name.singular_route_key + end + } + + route << + case record + when Symbol, String + record.to_s + when Class + @key_strategy.call record.model_name + else + if record.persisted? + args << record.to_model + record.to_model.class.model_name.singular_route_key + else + @key_strategy.call record.to_model.class.model_name + end + end + + route << suffix + + named_route = prefix + route.join("_") + [named_route, args] + end + + private + + def get_method_for_class(klass) + name = @key_strategy.call klass.model_name + prefix + "#{name}_#{suffix}" + end + + def get_method_for_string(str) + prefix + "#{str}_#{suffix}" + end + + [nil, 'new', 'edit'].each do |action| + CACHE['url'][action] = build action, 'url' + CACHE['path'][action] = build action, 'path' + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb new file mode 100644 index 0000000000..3c1c4fadf6 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -0,0 +1,191 @@ +require 'action_dispatch/http/request' +require 'active_support/core_ext/uri' +require 'active_support/core_ext/array/extract_options' +require 'rack/utils' +require 'action_controller/metal/exceptions' +require 'action_dispatch/routing/endpoint' + +module ActionDispatch + module Routing + class Redirect < Endpoint # :nodoc: + attr_reader :status, :block + + def initialize(status, block) + @status = status + @block = block + end + + def redirect?; true; end + + def call(env) + serve Request.new env + end + + def serve(req) + req.check_path_parameters! + uri = URI.parse(path(req.path_parameters, req)) + + unless uri.host + if relative_path?(uri.path) + uri.path = "#{req.script_name}/#{uri.path}" + elsif uri.path.empty? + uri.path = req.script_name.empty? ? "/" : req.script_name + end + end + + uri.scheme ||= req.scheme + uri.host ||= req.host + uri.port ||= req.port unless req.standard_port? + + body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>) + + headers = { + 'Location' => uri.to_s, + 'Content-Type' => 'text/html', + 'Content-Length' => body.length.to_s + } + + [ status, headers, [body] ] + end + + def path(params, request) + block.call params, request + end + + def inspect + "redirect(#{status})" + end + + private + def relative_path?(path) + path && !path.empty? && path[0] != '/' + end + + def escape(params) + Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }] + end + + def escape_fragment(params) + Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_fragment(v)] }] + end + + def escape_path(params) + Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_path(v)] }] + end + end + + class PathRedirect < Redirect + URL_PARTS = /\A([^?]+)?(\?[^#]+)?(#.+)?\z/ + + def path(params, request) + if block.match(URL_PARTS) + path = interpolation_required?($1, params) ? $1 % escape_path(params) : $1 + query = interpolation_required?($2, params) ? $2 % escape(params) : $2 + fragment = interpolation_required?($3, params) ? $3 % escape_fragment(params) : $3 + + "#{path}#{query}#{fragment}" + else + interpolation_required?(block, params) ? block % escape(params) : block + end + end + + def inspect + "redirect(#{status}, #{block})" + end + + private + def interpolation_required?(string, params) + !params.empty? && string && string.match(/%\{\w*\}/) + end + end + + class OptionRedirect < Redirect # :nodoc: + alias :options :block + + def path(params, request) + url_options = { + :protocol => request.protocol, + :host => request.host, + :port => request.optional_port, + :path => request.path, + :params => request.query_parameters + }.merge! options + + if !params.empty? && url_options[:path].match(/%\{\w*\}/) + url_options[:path] = (url_options[:path] % escape_path(params)) + end + + unless options[:host] || options[:domain] + if relative_path?(url_options[:path]) + url_options[:path] = "/#{url_options[:path]}" + url_options[:script_name] = request.script_name + elsif url_options[:path].empty? + url_options[:path] = request.script_name.empty? ? "/" : "" + url_options[:script_name] = request.script_name + end + end + + ActionDispatch::Http::URL.url_for url_options + end + + def inspect + "redirect(#{status}, #{options.map{ |k,v| "#{k}: #{v}" }.join(', ')})" + end + end + + module Redirection + + # Redirect any path to another path: + # + # get "/stories" => redirect("/posts") + # + # You can also use interpolation in the supplied redirect argument: + # + # get 'docs/:article', to: redirect('/wiki/%{article}') + # + # Note that if you return a path without a leading slash then the url is prefixed with the + # current SCRIPT_NAME environment variable. This is typically '/' but may be different in + # a mounted engine or where the application is deployed to a subdirectory of a website. + # + # Alternatively you can use one of the other syntaxes: + # + # The block version of redirect allows for the easy encapsulation of any logic associated with + # the redirect in question. Either the params and request are supplied as arguments, or just + # params, depending of how many arguments your block accepts. A string is required as a + # return value. + # + # get 'jokes/:number', to: redirect { |params, request| + # path = (params[:number].to_i.even? ? "wheres-the-beef" : "i-love-lamp") + # "http://#{request.host_with_port}/#{path}" + # } + # + # Note that the +do end+ syntax for the redirect block wouldn't work, as Ruby would pass + # the block to +get+ instead of +redirect+. Use <tt>{ ... }</tt> instead. + # + # The options version of redirect allows you to supply only the parts of the url which need + # to change, it also supports interpolation of the path similar to the first example. + # + # get 'stores/:name', to: redirect(subdomain: 'stores', path: '/%{name}') + # get 'stores/:name(*all)', to: redirect(subdomain: 'stores', path: '/%{name}%{all}') + # + # Finally, an object which responds to call can be supplied to redirect, allowing you to reuse + # common redirect routes. The call method must accept two arguments, params and request, and return + # a string. + # + # get 'accounts/:name' => redirect(SubdomainRedirector.new('api')) + # + def redirect(*args, &block) + options = args.extract_options! + status = options.delete(:status) || 301 + path = args.shift + + return OptionRedirect.new(status, options) if options.any? + return PathRedirect.new(status, path) if String === path + + block = path if path.respond_to? :call + raise ArgumentError, "redirection argument not supported" unless block + Redirect.new status, block + 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..5b3651aaee --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -0,0 +1,787 @@ +require 'action_dispatch/journey' +require 'forwardable' +require 'thread_safe' +require 'active_support/concern' +require 'active_support/core_ext/object/to_query' +require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/module/remove_method' +require 'active_support/core_ext/array/extract_options' +require 'action_controller/metal/exceptions' +require 'action_dispatch/http/request' +require 'action_dispatch/routing/endpoint' + +module ActionDispatch + module Routing + class RouteSet #:nodoc: + # Since the router holds references to many parts of the system + # like engines, controllers and the application itself, inspecting + # the route set can actually be really slow, therefore we default + # alias inspect to to_s. + alias inspect to_s + + class Dispatcher < Routing::Endpoint #:nodoc: + def initialize(defaults) + @defaults = defaults + @controller_class_names = ThreadSafe::Cache.new + end + + def dispatcher?; true; end + + def serve(req) + req.check_path_parameters! + params = req.path_parameters + + prepare_params!(params) + + # Just raise undefined constant errors if a controller was specified as default. + unless controller = controller(params, @defaults.key?(:controller)) + return [404, {'X-Cascade' => 'pass'}, []] + end + + dispatch(controller, params[:action], req.env) + end + + def prepare_params!(params) + normalize_controller!(params) + merge_default_action!(params) + end + + # If this is a default_controller (i.e. a controller specified by the user) + # we should raise an error in case it's not found, because it usually means + # a user error. However, if the controller was retrieved through a dynamic + # segment, as in :controller(/:action), we should simply return nil and + # delegate the control back to Rack cascade. Besides, if this is not a default + # controller, it means we should respect the @scope[:module] parameter. + def controller(params, default_controller=true) + if params && params.key?(:controller) + controller_param = params[:controller] + controller_reference(controller_param) + end + rescue NameError => e + raise ActionController::RoutingError, e.message, e.backtrace if default_controller + end + + private + + def controller_reference(controller_param) + const_name = @controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller" + ActiveSupport::Dependencies.constantize(const_name) + end + + def dispatch(controller, action, env) + controller.action(action).call(env) + end + + def normalize_controller!(params) + params[:controller] = params[:controller].underscore if params.key?(:controller) + end + + def merge_default_action!(params) + params[:action] ||= 'index' + 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, :url_helpers_module + + def initialize + @routes = {} + @path_helpers = Set.new + @url_helpers = Set.new + @url_helpers_module = Module.new + @path_helpers_module = Module.new + end + + def route_defined?(name) + key = name.to_sym + @path_helpers.include?(key) || @url_helpers.include?(key) + end + + def helper_names + @path_helpers.map(&:to_s) + @url_helpers.map(&:to_s) + end + + def clear! + @path_helpers.each do |helper| + @path_helpers_module.send :undef_method, helper + end + + @url_helpers.each do |helper| + @url_helpers_module.send :undef_method, helper + end + + @routes.clear + @path_helpers.clear + @url_helpers.clear + end + + def add(name, route) + key = name.to_sym + path_name = :"#{name}_path" + url_name = :"#{name}_url" + + if routes.key? key + @path_helpers_module.send :undef_method, path_name + @url_helpers_module.send :undef_method, url_name + end + routes[key] = route + define_url_helper @path_helpers_module, route, path_name, route.defaults, name, PATH + define_url_helper @url_helpers_module, route, url_name, route.defaults, name, FULL + + @path_helpers << path_name + @url_helpers << url_name + end + + def get(name) + routes[name.to_sym] + end + + def key?(name) + routes.key? 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 path_helpers_module(warn = false) + if warn + mod = @path_helpers_module + helpers = @path_helpers + Module.new do + include mod + + helpers.each do |meth| + define_method(meth) do |*args, &block| + ActiveSupport::Deprecation.warn("The method `#{meth}` cannot be used here as a full URL is required. Use `#{meth.to_s.sub(/_path$/, '_url')}` instead") + super(*args, &block) + end + end + end + else + @path_helpers_module + end + end + + class UrlHelper # :nodoc: + def self.create(route, options, route_name, url_strategy) + if optimize_helper?(route) + OptimizedUrlHelper.new(route, options, route_name, url_strategy) + else + new route, options, route_name, url_strategy + end + end + + def self.optimize_helper?(route) + !route.glob? && route.path.requirements.empty? + end + + attr_reader :url_strategy, :route_name + + class OptimizedUrlHelper < UrlHelper # :nodoc: + attr_reader :arg_size + + def initialize(route, options, route_name, url_strategy) + super + @required_parts = @route.required_parts + @arg_size = @required_parts.size + end + + def call(t, args, inner_options) + if args.size == arg_size && !inner_options && optimize_routes_generation?(t) + options = t.url_options.merge @options + options[:path] = optimized_helper(args) + url_strategy.call options + else + super + end + end + + private + + def optimized_helper(args) + params = parameterize_args(args) + missing_keys = missing_keys(params) + + unless missing_keys.empty? + raise_generation_error(params, missing_keys) + end + + @route.format params + end + + def optimize_routes_generation?(t) + t.send(:optimize_routes_generation?) + end + + def parameterize_args(args) + params = {} + @required_parts.zip(args.map(&:to_param)) { |k,v| params[k] = v } + params + end + + def missing_keys(args) + args.select{ |part, arg| arg.nil? || arg.empty? }.keys + end + + def raise_generation_error(args, missing_keys) + constraints = Hash[@route.requirements.merge(args).sort] + message = "No route matches #{constraints.inspect}" + message << " missing required keys: #{missing_keys.sort.inspect}" + + raise ActionController::UrlGenerationError, message + end + end + + def initialize(route, options, route_name, url_strategy) + @options = options + @segment_keys = route.segment_keys.uniq + @route = route + @url_strategy = url_strategy + @route_name = route_name + end + + def call(t, args, inner_options) + controller_options = t.url_options + options = controller_options.merge @options + hash = handle_positional_args(controller_options, + inner_options || {}, + args, + options, + @segment_keys) + + t._routes.url_for(hash, route_name, url_strategy) + end + + def handle_positional_args(controller_options, inner_options, args, result, path_params) + + if args.size > 0 + if args.size < path_params.size - 1 # take format into account + path_params -= controller_options.keys + path_params -= result.keys + end + path_params.each { |param| + result[param] = inner_options[param] || args.shift + } + end + + result.merge!(inner_options) + end + end + + private + # Create a url helper allowing 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') + # + def define_url_helper(mod, route, name, opts, route_key, url_strategy) + helper = UrlHelper.create(route, opts, route_key, url_strategy) + mod.module_eval do + define_method(name) do |*args| + options = nil + options = args.pop if args.last.is_a? Hash + helper.call self, args, options + end + end + end + end + + # :stopdoc: + # strategy for building urls to send to the client + PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) } + FULL = ->(options) { ActionDispatch::Http::URL.full_url_for(options) } + UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) } + # :startdoc: + + attr_accessor :formatter, :set, :named_routes, :default_scope, :router + attr_accessor :disable_clear_and_finalize, :resources_path_names + attr_accessor :default_url_options, :request_class + + alias :routes :set + + def self.default_resources_path_names + { :new => 'new', :edit => 'edit' } + end + + def initialize(request_class = ActionDispatch::Request) + self.named_routes = NamedRouteCollection.new + self.resources_path_names = self.class.default_resources_path_names + self.default_url_options = {} + self.request_class = request_class + + @append = [] + @prepend = [] + @disable_clear_and_finalize = false + @finalized = false + + @set = Journey::Routes.new + @router = Journey::Router.new @set + @formatter = Journey::Formatter.new @set + end + + def draw(&block) + clear! unless @disable_clear_and_finalize + eval_block(block) + finalize! unless @disable_clear_and_finalize + nil + end + + def append(&block) + @append << block + end + + def prepend(&block) + @prepend << block + end + + def eval_block(block) + if block.arity == 1 + raise "You are using the old router DSL which has been removed in Rails 3.1. " << + "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/" + end + mapper = Mapper.new(self) + if default_scope + mapper.with_default_scope(default_scope, &block) + else + mapper.instance_exec(&block) + end + end + private :eval_block + + def finalize! + return if @finalized + @append.each { |blk| eval_block(blk) } + @finalized = true + end + + def clear! + @finalized = false + named_routes.clear + set.clear + formatter.clear + @prepend.each { |blk| eval_block(blk) } + end + + def dispatcher(defaults) + Routing::RouteSet::Dispatcher.new(defaults) + end + + module MountedHelpers #:nodoc: + extend ActiveSupport::Concern + include UrlFor + end + + # Contains all the mounted helpers across different + # engines and the `main_app` helper for the application. + # You can include this in your classes if you want to + # access routes for other engines. + def mounted_helpers + MountedHelpers + end + + def define_mounted_helper(name) + return if MountedHelpers.method_defined?(name) + + routes = self + MountedHelpers.class_eval do + define_method "_#{name}" do + RoutesProxy.new(routes, _routes_context) + end + end + + MountedHelpers.class_eval(<<-RUBY, __FILE__, __LINE__ + 1) + def #{name} + @_#{name} ||= _#{name} + end + RUBY + end + + def url_helpers(include_path_helpers = true) + routes = self + + Module.new do + extend ActiveSupport::Concern + include UrlFor + + # Define url_for in the singleton level so one can do: + # Rails.application.routes.url_helpers.url_for(args) + @_routes = routes + class << self + delegate :url_for, :optimize_routes_generation?, to: '@_routes' + attr_reader :_routes + def url_options; {}; end + end + + url_helpers = routes.named_routes.url_helpers_module + + # Make named_routes available in the module singleton + # as well, so one can do: + # Rails.application.routes.url_helpers.posts_path + extend url_helpers + + # Any class that includes this module will get all + # named routes... + include url_helpers + + if include_path_helpers + path_helpers = routes.named_routes.path_helpers_module + else + path_helpers = routes.named_routes.path_helpers_module(true) + end + + include path_helpers + extend path_helpers + + # plus a singleton class method called _routes ... + included do + singleton_class.send(:redefine_method, :_routes) { routes } + end + + # And an instance method _routes. Note that + # UrlFor (included in this module) add extra + # conveniences for working with @_routes. + define_method(:_routes) { @_routes || routes } + end + end + + def empty? + routes.empty? + end + + def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true) + raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i) + + if name && named_routes[name] + raise ArgumentError, "Invalid route name, already in use: '#{name}' \n" \ + "You may have defined two routes with the same name using the `:as` option, or " \ + "you may be overriding a route already defined by a resource with the same naming. " \ + "For the latter, you can restrict the routes created with `resources` as explained here: \n" \ + "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created" + end + + path = conditions.delete :path_info + ast = conditions.delete :parsed_path_info + path = build_path(path, ast, requirements, anchor) + conditions = build_conditions(conditions, path.names.map { |x| x.to_sym }) + + route = @set.add_route(app, path, conditions, defaults, name) + named_routes[name] = route if name + route + end + + def build_path(path, ast, requirements, anchor) + strexp = Journey::Router::Strexp.new( + ast, + path, + requirements, + SEPARATORS, + anchor) + + pattern = Journey::Path::Pattern.new(strexp) + + builder = Journey::GTG::Builder.new pattern.spec + + # Get all the symbol nodes followed by literals that are not the + # dummy node. + symbols = pattern.spec.grep(Journey::Nodes::Symbol).find_all { |n| + builder.followpos(n).first.literal? + } + + # Get all the symbol nodes preceded by literals. + symbols.concat pattern.spec.find_all(&:literal?).map { |n| + builder.followpos(n).first + }.find_all(&:symbol?) + + symbols.each { |x| + x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/ + } + + pattern + end + private :build_path + + def build_conditions(current_conditions, path_values) + conditions = current_conditions.dup + + # Rack-Mount requires that :request_method be a regular expression. + # :request_method represents the HTTP verb that matches this route. + # + # Here we munge values before they get sent on to rack-mount. + verbs = conditions[:request_method] || [] + unless verbs.empty? + conditions[:request_method] = %r[^#{verbs.join('|')}$] + end + + conditions.keep_if do |k, _| + k == :action || k == :controller || k == :required_defaults || + @request_class.public_method_defined?(k) || path_values.include?(k) + end + end + private :build_conditions + + class Generator #:nodoc: + PARAMETERIZE = lambda do |name, value| + if name == :controller + value + elsif value.is_a?(Array) + value.map { |v| v.to_param }.join('/') + elsif param = value.to_param + param + end + end + + attr_reader :options, :recall, :set, :named_route + + def initialize(named_route, options, recall, set) + @named_route = named_route + @options = options.dup + @recall = recall.dup + @set = set + + normalize_recall! + normalize_options! + normalize_controller_action_id! + use_relative_controller! + normalize_controller! + normalize_action! + end + + def controller + @options[:controller] + end + + def current_controller + @recall[:controller] + end + + def use_recall_for(key) + if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key]) + if !named_route_exists? || segment_keys.include?(key) + @options[key] = @recall.delete(key) + end + end + end + + # Set 'index' as default action for recall + def normalize_recall! + @recall[:action] ||= 'index' + end + + def normalize_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". + + if options[:controller] + options[:action] ||= 'index' + options[:controller] = options[:controller].to_s + end + + if options.key?(:action) + options[:action] = (options[:action] || 'index').to_s + end + end + + # This pulls :controller, :action, and :id out of the recall. + # The recall key is only used if there is no key in the options + # or if the key in the options is identical. If any of + # :controller, :action or :id is not found, don't pull any + # more keys from the recall. + def normalize_controller_action_id! + use_recall_for(:controller) or return + use_recall_for(:action) or return + use_recall_for(:id) + end + + # if the current controller is "foo/bar/baz" and controller: "baz/bat" + # is specified, the controller becomes "foo/baz/bat" + def use_relative_controller! + if !named_route && different_controller? && !controller.start_with?("/") + old_parts = current_controller.split('/') + size = controller.count("/") + 1 + parts = old_parts[0...-size] << controller + @options[:controller] = parts.join("/") + end + end + + # Remove leading slashes from controllers + def normalize_controller! + @options[:controller] = controller.sub(%r{^/}, '') if controller + end + + # Move 'index' action from options to recall + def normalize_action! + if @options[:action] == 'index' + @recall[:action] = @options.delete(:action) + end + end + + # Generates a path from routes, returns [path, params]. + # If no route is generated the formatter will raise ActionController::UrlGenerationError + def generate + @set.formatter.generate(named_route, options, recall, PARAMETERIZE) + end + + def different_controller? + return false unless current_controller + controller.to_param != current_controller.to_param + end + + private + def named_route_exists? + named_route && set.named_routes[named_route] + end + + def segment_keys + set.named_routes[named_route].segment_keys + 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={}) + route_key = options.delete :use_route + path, params = generate(route_key, options, recall) + return path, params.keys + end + + def generate(route_key, options, recall = {}) + Generator.new(route_key, options, recall, self).generate + end + private :generate + + RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length, + :trailing_slash, :anchor, :params, :only_path, :script_name, + :original_script_name] + + def optimize_routes_generation? + default_url_options.empty? + end + + def find_script_name(options) + options.delete(:script_name) { '' } + end + + def path_for(options, route_name = nil) # :nodoc: + url_for(options, route_name, PATH) + end + + # The +options+ argument must be a hash whose keys are *symbols*. + def url_for(options, route_name = nil, url_strategy = UNKNOWN) + options = default_url_options.merge options + + user = password = nil + + if options[:user] && options[:password] + user = options.delete :user + password = options.delete :password + end + + recall = options.delete(:_recall) { {} } + + original_script_name = options.delete(:original_script_name) + script_name = find_script_name options + + if original_script_name + script_name = original_script_name + script_name + end + + path_options = options.dup + RESERVED_OPTIONS.each { |ro| path_options.delete ro } + + path, params = generate(route_name, path_options, recall) + + if options.key? :params + params.merge! options[:params] + end + + options[:path] = path + options[:script_name] = script_name + options[:params] = params + options[:user] = user + options[:password] = password + + url_strategy.call options + end + + def call(env) + req = request_class.new(env) + req.path_info = Journey::Router::Utils.normalize_path(req.path_info) + @router.serve(req) + end + + def recognize_path(path, environment = {}) + method = (environment[:method] || "GET").to_s.upcase + path = Journey::Router::Utils.normalize_path(path) unless path =~ %r{://} + extras = environment[:extras] || {} + + begin + env = Rack::MockRequest.env_for(path, {:method => method}) + rescue URI::InvalidURIError => e + raise ActionController::RoutingError, e.message + end + + req = request_class.new(env) + @router.recognize(req) do |route, params| + params.merge!(extras) + params.each do |key, value| + if value.is_a?(String) + value = value.dup.force_encoding(Encoding::BINARY) + params[key] = URI.parser.unescape(value) + end + end + old_params = req.path_parameters + req.path_parameters = old_params.merge params + app = route.app + if app.matches?(req) && app.dispatcher? + dispatcher = app.app + + if dispatcher.controller(params, false) + dispatcher.prepare_params!(params) + return params + else + raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller" + end + end + end + + raise ActionController::RoutingError, "No route matches #{path.inspect}" + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb new file mode 100644 index 0000000000..e2393d3799 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb @@ -0,0 +1,41 @@ +require 'active_support/core_ext/array/extract_options' + +module ActionDispatch + module Routing + class RoutesProxy #:nodoc: + include ActionDispatch::Routing::UrlFor + + attr_accessor :scope, :routes + alias :_routes :routes + + def initialize(routes, scope) + @routes, @scope = routes, scope + end + + def url_options + scope.send(:_with_routes, routes) do + scope.url_options + end + end + + def respond_to?(method, include_private = false) + super || routes.url_helpers.respond_to?(method) + end + + def method_missing(method, *args) + if routes.url_helpers.respond_to?(method) + self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method}(*args) + options = args.extract_options! + args << url_options.merge((options || {}).symbolize_keys) + routes.url_helpers.#{method}(*args) + end + RUBY + send(method, *args) + else + super + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb new file mode 100644 index 0000000000..eb554ec383 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -0,0 +1,189 @@ +module ActionDispatch + module Routing + # In <tt>config/routes.rb</tt> you define URL-to-controller mappings, but the reverse + # is also possible: an URL can be generated from one of your routing definitions. + # URL generation functionality is centralized in this module. + # + # See ActionDispatch::Routing for general information about routing and routes.rb. + # + # <b>Tip:</b> If you need to generate URLs from your models or some other place, + # then ActionController::UrlFor is what you're looking for. Read on for + # an introduction. In general, this module should not be included on its own, + # as it is usually included by url_helpers (as in Rails.application.routes.url_helpers). + # + # == URL generation from parameters + # + # As you may know, some functions, such as ActionController::Base#url_for + # and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set + # of parameters. For example, you've probably had the chance to write code + # like this in one of your views: + # + # <%= link_to('Click here', controller: 'users', + # action: 'new', message: 'Welcome!') %> + # # => <a href="/users/new?message=Welcome%21">Click here</a> + # + # link_to, and all other functions that require URL generation functionality, + # actually use ActionController::UrlFor under the hood. And in particular, + # they use the ActionController::UrlFor#url_for method. One can generate + # the same path as the above example by using the following code: + # + # include UrlFor + # url_for(controller: 'users', + # action: 'new', + # message: 'Welcome!', + # only_path: true) + # # => "/users/new?message=Welcome%21" + # + # Notice the <tt>only_path: true</tt> part. This is because UrlFor has no + # information about the website hostname that your Rails app is serving. So if you + # want to include the hostname as well, then you must also pass the <tt>:host</tt> + # argument: + # + # include UrlFor + # url_for(controller: 'users', + # action: 'new', + # message: 'Welcome!', + # host: 'www.example.com') + # # => "http://www.example.com/users/new?message=Welcome%21" + # + # By default, all controllers and views have access to a special version of url_for, + # that already knows what the current hostname is. So if you use url_for in your + # controllers or your views, then you don't need to explicitly pass the <tt>:host</tt> + # argument. + # + # For convenience reasons, mailers provide a shortcut for ActionController::UrlFor#url_for. + # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlFor#url_for' + # in full. However, mailers don't have hostname information, and that's why you'll still + # have to specify the <tt>:host</tt> argument when generating URLs in mailers. + # + # + # == URL generation for named routes + # + # UrlFor also allows one to access methods that have been auto-generated from + # named routes. For example, suppose that you have a 'users' resource in your + # <tt>config/routes.rb</tt>: + # + # resources :users + # + # This generates, among other things, the method <tt>users_path</tt>. By default, + # this method is accessible from your controllers, views and mailers. If you need + # to access this auto-generated method from other places (such as a model), then + # you can do that by including Rails.application.routes.url_helpers in your class: + # + # class User < ActiveRecord::Base + # include Rails.application.routes.url_helpers + # + # def base_uri + # user_path(self) + # end + # end + # + # User.find(1).base_uri # => "/users/1" + # + module UrlFor + extend ActiveSupport::Concern + include PolymorphicRoutes + + included do + unless method_defined?(:default_url_options) + # Including in a class uses an inheritable hash. Modules get a plain hash. + if respond_to?(:class_attribute) + class_attribute :default_url_options + else + mattr_writer :default_url_options + end + + self.default_url_options = {} + end + + include(*_url_for_modules) if respond_to?(:_url_for_modules) + end + + def initialize(*) + @_routes = nil + super + end + + # Hook overridden in controller to add request information + # with `default_url_options`. Application logic should not + # go into url_options. + def url_options + default_url_options + end + + # Generate a url based on the options provided, default_url_options and the + # routes defined in routes.rb. The following options are supported: + # + # * <tt>:only_path</tt> - If true, the relative url is returned. Defaults to +false+. + # * <tt>:protocol</tt> - The protocol to connect to. Defaults to 'http'. + # * <tt>:host</tt> - Specifies the host the link should be targeted at. + # If <tt>:only_path</tt> is false, this option must be + # provided either explicitly, or via +default_url_options+. + # * <tt>:subdomain</tt> - Specifies the subdomain of the link, using the +tld_length+ + # to split the subdomain from the host. + # If false, removes all subdomains from the host part of the link. + # * <tt>:domain</tt> - Specifies the domain of the link, using the +tld_length+ + # to split the domain from the host. + # * <tt>:tld_length</tt> - Number of labels the TLD id composed of, only used if + # <tt>:subdomain</tt> or <tt>:domain</tt> are supplied. Defaults to + # <tt>ActionDispatch::Http::URL.tld_length</tt>, which in turn defaults to 1. + # * <tt>:port</tt> - Optionally specify the port to connect to. + # * <tt>:anchor</tt> - An anchor name to be appended to the path. + # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/" + # * <tt>:script_name</tt> - Specifies application path relative to domain root. If provided, prepends application path. + # + # Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to + # +url_for+ is forwarded to the Routes module. + # + # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', port: '8080' + # # => 'http://somehost.org:8080/tasks/testing' + # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', anchor: 'ok', only_path: true + # # => '/tasks/testing#ok' + # url_for controller: 'tasks', action: 'testing', trailing_slash: true + # # => 'http://somehost.org/tasks/testing/' + # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', number: '33' + # # => 'http://somehost.org/tasks/testing?number=33' + # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp" + # # => 'http://somehost.org/myapp/tasks/testing' + # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp", only_path: true + # # => '/myapp/tasks/testing' + def url_for(options = nil) + case options + when nil + _routes.url_for(url_options.symbolize_keys) + when Hash + route_name = options.delete :use_route + _routes.url_for(options.symbolize_keys.reverse_merge!(url_options), + route_name) + when String + options + when Symbol + HelperMethodBuilder.url.handle_string_call self, options + when Array + polymorphic_url(options, options.extract_options!) + when Class + HelperMethodBuilder.url.handle_class_call self, options + else + HelperMethodBuilder.url.handle_model_call self, options + end + end + + protected + + def optimize_routes_generation? + _routes.optimize_routes_generation? && default_url_options.empty? + end + + def _with_routes(routes) + old_routes, @_routes = @_routes, routes + yield + ensure + @_routes = old_routes + end + + def _routes_context + self + end + end + end +end |