aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/routing
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_dispatch/routing')
-rw-r--r--actionpack/lib/action_dispatch/routing/endpoint.rb10
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb210
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb2106
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb324
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb191
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb755
-rw-r--r--actionpack/lib/action_dispatch/routing/routes_proxy.rb42
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb210
8 files changed, 3848 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..f3a5268d2e
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -0,0 +1,210 @@
+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 path
+ super.spec.to_s
+ end
+
+ def name
+ super.to_s
+ end
+
+ def reqs
+ @reqs ||= begin
+ reqs = endpoint
+ reqs += " #{constraints}" 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)}
+ 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(&:internal?).collect do |route|
+ collect_engine_routes(route)
+
+ { name: route.name,
+ verb: route.verb,
+ path: route.path,
+ reqs: route.reqs }
+ 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..18cd205bad
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -0,0 +1,2106 @@
+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/regexp'
+require 'action_dispatch/routing/redirection'
+require 'action_dispatch/routing/endpoint'
+
+module ActionDispatch
+ module Routing
+ class Mapper
+ URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
+
+ class Constraints < Routing::Endpoint #:nodoc:
+ attr_reader :app, :constraints
+
+ SERVE = ->(app, req) { app.serve req }
+ CALL = ->(app, req) { app.call req.env }
+
+ def initialize(app, constraints, strategy)
+ # 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
+
+ @strategy = strategy
+
+ @app, @constraints, = app, constraints
+ end
+
+ def dispatcher?; @strategy == SERVE; 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)
+
+ @strategy.call @app, req
+ 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, :defaults
+ attr_reader :to, :default_controller, :default_action
+ attr_reader :required_defaults, :ast
+
+ def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
+ options = scope[:options].merge(options) if scope[:options]
+
+ defaults = (scope[:defaults] || {}).dup
+ scope_constraints = scope[:constraints] || {}
+
+ new set, ast, defaults, controller, default_action, scope[:module], to, formatted, scope_constraints, scope[:blocks] || [], via, options_constraints, anchor, options
+ end
+
+ def self.check_via(via)
+ 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
+ via
+ end
+
+ def self.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 self.optional_format?(path, format)
+ format != false && !path.include?(':format') && !path.end_with?('/')
+ end
+
+ def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options)
+ @defaults = defaults
+ @set = set
+
+ @to = to
+ @default_controller = controller
+ @default_action = default_action
+ @ast = ast
+ @anchor = anchor
+ @via = via
+
+ path_params = ast.find_all(&:symbol?).map(&:to_sym)
+
+ options = add_wildcard_options(options, formatted, ast)
+
+ options = normalize_options!(options, path_params, modyoule)
+
+ split_options = constraints(options, path_params)
+
+ constraints = scope_constraints.merge Hash[split_options[:constraints] || []]
+
+ if options_constraints.is_a?(Hash)
+ @defaults = Hash[options_constraints.find_all { |key, default|
+ URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
+ }].merge @defaults
+ @blocks = blocks
+ constraints.merge! options_constraints
+ else
+ @blocks = blocks(options_constraints)
+ end
+
+ requirements, conditions = split_constraints path_params, constraints
+ verify_regexp_requirements requirements.map(&:last).grep(Regexp)
+
+ formats = normalize_format(formatted)
+
+ @requirements = formats[:requirements].merge Hash[requirements]
+ @conditions = Hash[conditions]
+ @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))
+
+ @required_defaults = (split_options[:required_defaults] || []).map(&:first)
+ end
+
+ def make_route(name, precedence)
+ route = Journey::Route.new(name,
+ application,
+ path,
+ conditions,
+ required_defaults,
+ defaults,
+ request_method,
+ precedence)
+
+ route
+ end
+
+ def application
+ app(@blocks)
+ end
+
+ def path
+ build_path @ast, requirements, @anchor
+ end
+
+ def conditions
+ build_conditions @conditions, @set.request_class
+ end
+
+ def build_conditions(current_conditions, request_class)
+ conditions = current_conditions.dup
+
+ conditions.keep_if do |k, _|
+ request_class.public_method_defined?(k)
+ end
+ end
+ private :build_conditions
+
+ def request_method
+ @via.map { |x| Journey::Route.verb_matcher(x) }
+ end
+ private :request_method
+
+ JOINED_SEPARATORS = SEPARATORS.join # :nodoc:
+
+ def build_path(ast, requirements, anchor)
+ pattern = Journey::Path::Pattern.new(ast, requirements, JOINED_SEPARATORS, anchor)
+
+ # Get all the symbol nodes followed by literals that are not the
+ # dummy node.
+ symbols = ast.find_all { |n|
+ n.cat? && n.left.symbol? && n.right.cat? && n.right.left.literal?
+ }.map(&:left)
+
+ # Get all the symbol nodes preceded by literals.
+ symbols.concat ast.find_all { |n|
+ n.cat? && n.left.literal? && n.right.cat? && n.right.left.symbol?
+ }.map { |n| n.right.left }
+
+ symbols.each { |x|
+ x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/
+ }
+
+ pattern
+ end
+ private :build_path
+
+
+ private
+ def add_wildcard_options(options, formatted, path_ast)
+ # 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).each_with_object({}) { |node, hash|
+ hash[node.name.to_sym] ||= /.+?/
+ }.merge options
+ else
+ options
+ end
+ end
+
+ def normalize_options!(options, path_params, modyoule)
+ 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.partition do |key, requirement|
+ path_params.include?(key) || key == :controller
+ end
+ end
+
+ def normalize_format(formatted)
+ case formatted
+ when true
+ { requirements: { format: /.+/ },
+ defaults: {} }
+ when Regexp
+ { requirements: { format: formatted },
+ defaults: { format: nil } }
+ when String
+ { requirements: { format: Regexp.compile(formatted) },
+ defaults: { format: formatted } }
+ else
+ { requirements: { }, defaults: { } }
+ end
+ end
+
+ def verify_regexp_requirements(requirements)
+ requirements.each do |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
+ end
+
+ def normalize_defaults(options)
+ Hash[options.reject { |_, default| Regexp === default }]
+ end
+
+ def app(blocks)
+ if to.is_a?(Class) && to < ActionController::Metal
+ Routing::RouteSet::StaticDispatcher.new to
+ else
+ if to.respond_to?(:call)
+ Constraints.new(to, blocks, Constraints::CALL)
+ elsif blocks.any?
+ Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
+ else
+ dispatcher(defaults.key?(:controller))
+ 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)
+ if to =~ /#/
+ to.split('#')
+ 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(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
+ [callable_constraint]
+ end
+
+ def constraints(options, path_params)
+ options.group_by do |key, option|
+ if Regexp === option
+ :constraints
+ else
+ if path_params.include?(key)
+ :path_params
+ else
+ :required_defaults
+ end
+ end
+ end
+ end
+
+ def dispatcher(raise_on_name_error)
+ Routing::RouteSet::Dispatcher.new raise_on_name_error
+ 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 = {})
+ name = has_named_route?(:root) ? nil : :root
+ match '/', { as: name, 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: -> (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 HttpHelpers[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>.
+ # In your router:
+ #
+ # resources :user, param: :name
+ #
+ # You can override <tt>ActiveRecord::Base#to_param</tt> of a related
+ # model to construct a URL:
+ #
+ # class User < ActiveRecord::Base
+ # def to_param
+ # name
+ # end
+ # end
+ #
+ # user = User.find_by(name: 'Phusion')
+ # user_path(user) # => "/users/Phusion"
+ #
+ # [: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: -> (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)
+ elsif Hash === app
+ options = app
+ app, path = options.find { |k, _| k.respond_to?(:call) }
+ options.delete(app) if app
+ end
+
+ raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
+ raise ArgumentError, <<-MSG.strip_heredoc unless path
+ Must be called with mount point
+
+ mount SomeRackApp, at: "some_route"
+ or
+ mount(SomeRackApp => "some_route")
+ MSG
+
+ rails_app = rails_app? app
+ options[:as] ||= app_name(app, rails_app)
+
+ 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.key? name
+ end
+
+ private
+ def rails_app?(app)
+ app.is_a?(Class) && app < Rails::Railtie
+ end
+
+ def app_name(app, rails_app)
+ if rails_app
+ app.railtie_name
+ elsif app.is_a?(Class)
+ class_name = app.name
+ ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
+ end
+ 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)
+ prefix_options[:relative_url_root] = ''.freeze
+ # 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
+ if options.key?(:defaults)
+ defaults(options.delete(:defaults)) { match(*args, options, &block) }
+ else
+ match(*args, options, &block)
+ end
+ 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
+
+ if options.key?(:only) || options.key?(:except)
+ scope[:action_options] = { only: options.delete(:only),
+ except: options.delete(:except) }
+ end
+
+ if options.key? :anchor
+ raise ArgumentError, 'anchor is ignored unless passed to `match`'
+ end
+
+ @scope.options.each do |option|
+ if option == :blocks
+ value = block
+ elsif option == :options
+ value = options
+ else
+ value = options.delete(option) { POISON }
+ end
+
+ unless POISON == value
+ scope[option] = send("merge_#{option}_scope", @scope[option], value)
+ end
+ end
+
+ @scope = @scope.new scope
+ yield
+ self
+ ensure
+ @scope = @scope.parent
+ end
+
+ POISON = Object.new # :nodoc:
+
+ # Scopes routes to a specific controller
+ #
+ # controller "food" do
+ # match "bacon", action: :bacon, via: :get
+ # end
+ def controller(controller)
+ @scope = @scope.new(controller: controller)
+ yield
+ ensure
+ @scope = @scope.parent
+ 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,
+ as: options.fetch(:as, path),
+ shallow_path: options.fetch(:path, path),
+ shallow_prefix: options.fetch(:as, path)
+ }
+
+ path_scope(options.delete(:path) { path }) do
+ scope(defaults.merge!(options)) { yield }
+ end
+ 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(-> (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 = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults))
+ yield
+ ensure
+ @scope = @scope.parent
+ 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_via_scope(parent, child) #:nodoc:
+ child
+ end
+
+ def merge_format_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 || {}).merge(child)
+ end
+
+ def merge_shallow_scope(parent, child) #:nodoc:
+ child ? true : false
+ 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)
+
+ class Resource #:nodoc:
+ attr_reader :controller, :path, :param
+
+ def initialize(entities, api_only, shallow, 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 = shallow
+ @api_only = api_only
+ @only = options.delete :only
+ @except = options.delete :except
+ end
+
+ def default_actions
+ if @api_only
+ [:index, :create, :show, :update, :destroy]
+ else
+ [:index, :create, :new, :show, :update, :destroy, :edit]
+ end
+ end
+
+ def actions
+ if @only
+ Array(@only).map(&:to_sym)
+ elsif @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
+ 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?
+ @shallow
+ end
+
+ def singleton?; false; end
+ end
+
+ class SingletonResource < Resource #:nodoc:
+ def initialize(entities, api_only, shallow, options)
+ super
+ @as = nil
+ @controller = (options[:controller] || plural).to_s
+ @as = options[:as]
+ end
+
+ def default_actions
+ if @api_only
+ [:show, :create, :update, :destroy]
+ else
+ [:show, :create, :update, :destroy, :new, :edit]
+ end
+ 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
+
+ def singleton?; true; end
+ 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
+
+ with_scope_level(:resource) do
+ options = apply_action_options options
+ resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], 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
+ 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
+
+ with_scope_level(:resources) do
+ options = apply_action_options options
+ resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], 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
+ 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
+ path_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 {
+ path_scope(parent_resource.member_scope) { yield }
+ }
+ else
+ path_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
+ path_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 do
+ path_scope(parent_resource.nested_scope) do
+ scope(nested_options) { yield }
+ end
+ end
+ else
+ path_scope(parent_resource.nested_scope) do
+ scope(nested_options) { yield }
+ end
+ 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 = @scope.new(shallow: true)
+ yield
+ ensure
+ @scope = @scope.parent
+ end
+
+ def shallow?
+ !parent_resource.singleton? && @scope[:shallow]
+ end
+
+ # Matches a url pattern to one or more routes.
+ # For more information, see match[rdoc-ref:Base#match].
+ #
+ # match 'path' => 'controller#action', via: patch
+ # match 'path', to: 'controller#action', via: :post
+ # 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
+
+ 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
+
+ controller = options.delete(:controller) || @scope[:controller]
+ option_path = options.delete :path
+ to = options.delete :to
+ via = Mapping.check_via Array(options.delete(:via) {
+ @scope[:via]
+ })
+ formatted = options.delete(:format) { @scope[:format] }
+ anchor = options.delete(:anchor) { true }
+ options_constraints = options.delete(:constraints) || {}
+
+ path_types = paths.group_by(&:class)
+ path_types.fetch(String, []).each do |_path|
+ route_options = options.dup
+ if _path && option_path
+ ActiveSupport::Deprecation.warn <<-eowarn
+Specifying strings for both :path and the route path is deprecated. Change things like this:
+
+ match #{_path.inspect}, :path => #{option_path.inspect}
+
+to this:
+
+ match #{option_path.inspect}, :as => #{_path.inspect}, :action => #{path.inspect}
+ eowarn
+ route_options[:action] = _path
+ route_options[:as] = _path
+ _path = option_path
+ end
+ to = get_to_from_path(_path, to, route_options[:action])
+ decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
+ end
+
+ path_types.fetch(Symbol, []).each do |action|
+ route_options = options.dup
+ decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints)
+ end
+
+ self
+ end
+
+ def get_to_from_path(path, to, action)
+ return to if to || action
+
+ path_without_format = path.sub(/\(\.:format\)$/, '')
+ if using_match_shorthand?(path_without_format)
+ path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1').tr("-", "_")
+ else
+ nil
+ end
+ end
+
+ def using_match_shorthand?(path)
+ path =~ %r{^/?[-\w]+/[-\w/]+$}
+ end
+
+ def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc:
+ if on = options.delete(:on)
+ send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
+ else
+ case @scope.scope_level
+ when :resources
+ nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
+ when :resource
+ member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
+ else
+ add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
+ end
+ end
+ end
+
+ def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc:
+ path = path_for_action(action, _path)
+ raise ArgumentError, "path is required" if path.blank?
+
+ action = action.to_s
+
+ default_action = options.delete(:action) || @scope[:action]
+
+ if action =~ /^[\w\-\/]+$/
+ default_action ||= action.tr('-', '_') unless action.include?("/")
+ else
+ action = nil
+ end
+
+ as = if !options.fetch(:as, true) # if it's set to nil or false
+ options.delete(:as)
+ else
+ name_for_action(options.delete(:as), action)
+ end
+
+ path = Mapping.normalize_path URI.parser.escape(path), formatted
+ ast = Journey::Parser.parse path
+
+ mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
+ @set.add_route(mapping, ast, 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.resources?
+ with_scope_level(:root) do
+ path_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
+
+ false
+ end
+
+ def apply_action_options(options) # :nodoc:
+ return options if action_options? options
+ options.merge scope_action_options
+ end
+
+ def action_options?(options) #:nodoc:
+ options[:only] || options[:except]
+ end
+
+ def scope_action_options #:nodoc:
+ @scope[:action_options] || {}
+ end
+
+ def resource_scope? #:nodoc:
+ @scope.resource_scope?
+ end
+
+ def resource_method_scope? #:nodoc:
+ @scope.resource_method_scope?
+ end
+
+ def nested_scope? #:nodoc:
+ @scope.nested?
+ end
+
+ def with_scope_level(kind)
+ @scope = @scope.new_level(kind)
+ yield
+ ensure
+ @scope = @scope.parent
+ end
+
+ def resource_scope(resource) #:nodoc:
+ @scope = @scope.new(:scope_level_resource => resource)
+
+ controller(resource.resource_scope) { yield }
+ ensure
+ @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 shallow_nesting_depth #:nodoc:
+ @scope.find_all { |node|
+ node.frame[:scope_level_resource]
+ }.count { |node| node.frame[:scope_level_resource].shallow? }
+ 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) #:nodoc:
+ resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
+ end
+
+ def shallow_scope #:nodoc:
+ scope = { :as => @scope[:shallow_prefix],
+ :path => @scope[:shallow_path] }
+ @scope = @scope.new scope
+
+ yield
+ ensure
+ @scope = @scope.parent
+ end
+
+ def path_for_action(action, path) #:nodoc:
+ return "#{@scope[:path]}/#{path}" if path
+
+ if canonical_action?(action)
+ @scope[:path].to_s
+ else
+ "#{@scope[:path]}/#{action_path(action)}"
+ end
+ end
+
+ def action_path(name) #:nodoc:
+ @scope[:path_names][name.to_sym] || name
+ end
+
+ def prefix_name_for_action(as, action) #:nodoc:
+ if as
+ prefix = as
+ elsif !canonical_action?(action)
+ prefix = action
+ end
+
+ if prefix && prefix != '/' && !prefix.empty?
+ Mapper.normalize_name prefix.to_s.tr('-', '_')
+ end
+ end
+
+ def name_for_action(as, action) #:nodoc:
+ prefix = prefix_name_for_action(as, action)
+ 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
+
+ action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
+ candidate = action_name.select(&:present?).join('_')
+
+ unless candidate.empty?
+ # 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 || has_named_route?(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
+
+ def api_only?
+ @set.api_only?
+ end
+ private
+
+ def path_scope(path)
+ @scope = @scope.new(path: merge_path_scope(@scope[:path], path))
+ yield
+ ensure
+ @scope = @scope.parent
+ 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, :via, :format, :options]
+
+ RESOURCE_SCOPES = [:resource, :resources]
+ RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
+
+ attr_reader :parent, :scope_level
+
+ def initialize(hash, parent = NULL, scope_level = nil)
+ @hash = hash
+ @parent = parent
+ @scope_level = scope_level
+ end
+
+ def nested?
+ scope_level == :nested
+ end
+
+ def resources?
+ scope_level == :resources
+ end
+
+ def resource_method_scope?
+ RESOURCE_METHOD_SCOPES.include? scope_level
+ end
+
+ def action_name(name_prefix, prefix, collection_name, member_name)
+ case 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
+ end
+
+ def resource_scope?
+ RESOURCE_SCOPES.include? scope_level
+ end
+
+ def options
+ OPTIONS
+ end
+
+ def new(hash)
+ self.class.new hash, self, scope_level
+ end
+
+ def new_level(level)
+ self.class.new(frame, self, level)
+ end
+
+ def [](key)
+ scope = find { |node| node.frame.key? key }
+ scope && scope.frame[key]
+ end
+
+ include Enumerable
+
+ def each
+ node = self
+ loop do
+ break if node.equal? NULL
+ yield node
+ node = node.parent
+ end
+ end
+
+ def frame; @hash; end
+
+ NULL = Scope.new(nil, nil)
+ end
+
+ def initialize(set) #:nodoc:
+ @set = set
+ @scope = Scope.new({ :path_names => @set.resources_path_names })
+ @concerns = {}
+ 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..9934f5547a
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -0,0 +1,324 @@
+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
+ # 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 = {})
+ polymorphic_url_for_action("#{action}", record_or_hash, options)
+ end
+
+ def #{action}_polymorphic_path(record_or_hash, options = {})
+ polymorphic_path_for_action("#{action}", record_or_hash, options)
+ end
+ EOT
+ end
+
+ private
+
+ def polymorphic_url_for_action(action, record_or_hash, options)
+ polymorphic_url(record_or_hash, options.merge(:action => action))
+ end
+
+ def polymorphic_path_for_action(action, record_or_hash, options)
+ polymorphic_path(record_or_hash, options.merge(:action => action))
+ end
+
+ 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
+ record_or_hash_or_array = record_or_hash_or_array.compact
+ if record_or_hash_or_array.empty?
+ 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
+ named_route = if model.persisted?
+ args << model
+ get_method_for_string model.model_name.singular_route_key
+ else
+ get_method_for_class model
+ end
+
+ [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.model_name.singular_route_key
+ end
+ }
+
+ route <<
+ case record
+ when Symbol, String
+ record.to_s
+ when Class
+ @key_strategy.call record.model_name
+ else
+ model = record.to_model
+ if model.persisted?
+ args << model
+ model.model_name.singular_route_key
+ else
+ @key_strategy.call model.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
+ get_method_for_string name
+ 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..d6987f4d09
--- /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..c4228df925
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -0,0 +1,755 @@
+require 'action_dispatch/journey'
+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
+ # :stopdoc:
+ class RouteSet
+ # 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
+ def initialize(raise_on_name_error)
+ @raise_on_name_error = raise_on_name_error
+ end
+
+ def dispatcher?; true; end
+
+ def serve(req)
+ params = req.path_parameters
+ controller = controller req
+ res = controller.make_response! req
+ dispatch(controller, params[:action], req, res)
+ rescue NameError => e
+ if @raise_on_name_error
+ raise ActionController::RoutingError, e.message, e.backtrace
+ else
+ return [404, {'X-Cascade' => 'pass'}, []]
+ end
+ end
+
+ private
+
+ def controller(req)
+ req.controller_class
+ end
+
+ def dispatch(controller, action, req, res)
+ controller.dispatch(action, req, res)
+ end
+ end
+
+ class StaticDispatcher < Dispatcher
+ def initialize(controller_class)
+ super(false)
+ @controller_class = controller_class
+ end
+
+ private
+
+ def controller(_); @controller_class; 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
+ include Enumerable
+ attr_reader :routes, :url_helpers_module, :path_helpers_module
+ private :routes
+
+ 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, UNKNOWN
+
+ @path_helpers << path_name
+ @url_helpers << url_name
+ end
+
+ def get(name)
+ routes[name.to_sym]
+ end
+
+ def key?(name)
+ return unless 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
+
+ class UrlHelper
+ 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
+ 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) do
+ raise_generation_error(args)
+ end
+
+ @route.format params
+ end
+
+ def optimize_routes_generation?(t)
+ t.send(:optimize_routes_generation?)
+ end
+
+ def parameterize_args(args)
+ params = {}
+ @arg_size.times { |i|
+ key = @required_parts[i]
+ value = args[i].to_param
+ yield key if value.nil? || value.empty?
+ params[key] = value
+ }
+ params
+ end
+
+ def raise_generation_error(args)
+ missing_keys = []
+ params = parameterize_args(args) { |missing_key|
+ missing_keys << missing_key
+ }
+ constraints = Hash[@route.requirements.merge(params).sort_by{|k,v| k.to_s}]
+ 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
+ # take format into account
+ if path_params.include?(:format)
+ path_params_size = path_params.size - 1
+ else
+ path_params_size = path_params.size
+ end
+
+ if args.size < path_params_size
+ path_params -= controller_options.keys
+ path_params -= result.keys
+ end
+ inner_options.each_key do |key|
+ path_params.delete(key)
+ end
+
+ args.each_with_index do |arg, index|
+ param = path_params[index]
+ result[param] = arg if param
+ end
+ 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
+
+ # strategy for building urls to send to the client
+ PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) }
+ UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) }
+
+ attr_accessor :formatter, :set, :named_routes, :default_scope, :router
+ attr_accessor :disable_clear_and_finalize, :resources_path_names
+ attr_accessor :default_url_options
+ attr_reader :env_key
+
+ alias :routes :set
+
+ def self.default_resources_path_names
+ { :new => 'new', :edit => 'edit' }
+ end
+
+ def self.new_with_config(config)
+ route_set_config = DEFAULT_CONFIG
+
+ # engines apparently don't have this set
+ if config.respond_to? :relative_url_root
+ route_set_config.relative_url_root = config.relative_url_root
+ end
+
+ if config.respond_to? :api_only
+ route_set_config.api_only = config.api_only
+ end
+
+ new route_set_config
+ end
+
+ Config = Struct.new :relative_url_root, :api_only
+
+ DEFAULT_CONFIG = Config.new(nil, false)
+
+ def initialize(config = DEFAULT_CONFIG)
+ self.named_routes = NamedRouteCollection.new
+ self.resources_path_names = self.class.default_resources_path_names
+ self.default_url_options = {}
+
+ @config = config
+ @append = []
+ @prepend = []
+ @disable_clear_and_finalize = false
+ @finalized = false
+ @env_key = "ROUTES_#{object_id}_SCRIPT_NAME".freeze
+
+ @set = Journey::Routes.new
+ @router = Journey::Router.new @set
+ @formatter = Journey::Formatter.new self
+ end
+
+ def relative_url_root
+ @config.relative_url_root
+ end
+
+ def api_only?
+ @config.api_only
+ end
+
+ def request_class
+ ActionDispatch::Request
+ end
+
+ def make_request(env)
+ request_class.new env
+ end
+ private :make_request
+
+ 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)
+ 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
+
+ module MountedHelpers
+ 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
+ helpers = routes.url_helpers
+
+ MountedHelpers.class_eval do
+ define_method "_#{name}" do
+ RoutesProxy.new(routes, _routes_context, helpers)
+ end
+ end
+
+ MountedHelpers.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
+ def #{name}
+ @_#{name} ||= _#{name}
+ end
+ RUBY
+ end
+
+ def url_helpers(supports_path = 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
+ def url_for(options)
+ @_routes.url_for(options)
+ end
+
+ def optimize_routes_generation?
+ @_routes.optimize_routes_generation?
+ end
+
+ 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 supports_path
+ path_helpers = routes.named_routes.path_helpers_module
+
+ include path_helpers
+ extend path_helpers
+ end
+
+ # 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 }
+
+ define_method(:_generate_paths_by_default) do
+ supports_path
+ end
+
+ private :_generate_paths_by_default
+ end
+ end
+
+ def empty?
+ routes.empty?
+ end
+
+ def add_route(mapping, path_ast, name, anchor)
+ 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
+
+ route = @set.add_route(name, mapping)
+ named_routes[name] = route if name
+ route
+ end
+
+ class Generator
+ PARAMETERIZE = lambda do |name, value|
+ if name == :controller
+ value
+ else
+ value.to_param
+ end
+ end
+
+ attr_reader :options, :recall, :set, :named_route
+
+ def initialize(named_route, options, recall, set)
+ @named_route = named_route
+ @options = options
+ @recall = recall
+ @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[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!
+ if controller
+ if controller.start_with?("/".freeze)
+ @options[:controller] = controller[1..-1]
+ else
+ @options[:controller] = controller
+ end
+ end
+ end
+
+ # Move 'index' action from options to recall
+ def normalize_action!
+ if @options[:action] == 'index'.freeze
+ @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, :relative_url_root]
+
+ def optimize_routes_generation?
+ default_url_options.empty?
+ end
+
+ def find_script_name(options)
+ options.delete(:script_name) || find_relative_url_root(options) || ''
+ end
+
+ def find_relative_url_root(options)
+ options.delete(:relative_url_root) || relative_url_root
+ end
+
+ def path_for(options, route_name = nil)
+ 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 = make_request(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 = make_request(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?
+ begin
+ req.controller_class
+ rescue NameError
+ raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller"
+ end
+
+ return req.path_parameters
+ end
+ end
+
+ raise ActionController::RoutingError, "No route matches #{path.inspect}"
+ end
+ end
+ # :startdoc:
+ 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..040ea04046
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
@@ -0,0 +1,42 @@
+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, helpers)
+ @routes, @scope = routes, scope
+ @helpers = helpers
+ end
+
+ def url_options
+ scope.send(:_with_routes, routes) do
+ scope.url_options
+ end
+ end
+
+ def respond_to?(method, include_private = false)
+ super || @helpers.respond_to?(method)
+ end
+
+ def method_missing(method, *args)
+ if @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)
+ @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..b6c031dcf4
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -0,0 +1,210 @@
+module ActionDispatch
+ module Routing
+ # In <tt>config/routes.rb</tt> you define URL-to-controller mappings, but the reverse
+ # is also possible: a 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 you still have to provide
+ # the +:host+ argument or set the default host that will be used in all mailers using the
+ # configuration option +config.action_mailer.default_url_options+. For more information on
+ # url_for in mailers read the ActionMailer#Base documentation.
+ #
+ #
+ # == 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'
+ #
+ # Missing routes keys may be filled in from the current request's parameters
+ # (e.g. +:controller+, +:action+, +:id+ and any other parameters that are
+ # placed in the path). Given that the current action has been reached
+ # through `GET /users/1`:
+ #
+ # url_for(only_path: true) # => '/users/1'
+ # url_for(only_path: true, action: 'edit') # => '/users/1/edit'
+ # url_for(only_path: true, action: 'edit', id: 2) # => '/users/2/edit'
+ #
+ # Notice that no +:id+ parameter was provided to the first +url_for+ call
+ # and the helper used the one from the route's path. Any path parameter
+ # implicitly used by +url_for+ can always be overwritten like shown on the
+ # last +url_for+ calls.
+ 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 ActionController::Parameters
+ route_name = options.delete :use_route
+ _routes.url_for(options.to_unsafe_h.symbolize_keys.
+ reverse_merge!(url_options), route_name)
+ when String
+ options
+ when Symbol
+ HelperMethodBuilder.url.handle_string_call self, options
+ when Array
+ components = options.dup
+ polymorphic_url(components, components.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