1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/actionpack/lib/action_controller/routing.rb

264 lines
12 KiB
Ruby
Raw Normal View History

module ActionController
module Routing
ROUTE_FILE = defined?(RAILS_ROOT) ? File.expand_path(File.join(RAILS_ROOT, 'config', 'routes')) : nil
class Route
attr_reader :defaults # The defaults hash
def initialize(path, hash={})
raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash)
@defaults = {}
@requirements = {}
self.items = path
hash.each do |k, v|
raise TypeError, "Hash may only contain symbols!" unless k.kind_of? Symbol
(@items.include?(k) ? @defaults : @requirements)[k] = v
end
# Add in defaults for :action and :id.
[[:action, 'index'], [:id, nil]].each do |name, default|
@defaults[name] = default if @items.include?(name) && ! (@requirements.key?(name) || @defaults.key?(name))
end
end
# Generate a URL given the provided options.
# All values in options should be symbols.
# Returns the path and the unused names in a 2 element array.
# If generation fails, [nil, nil] is returned
# Generation can fail because of a missing value, or because an equality check fails.
#
# Generate urls will be as short as possible. If the last component of a url is equal to the default value,
# then that component is removed. This is applied as many times as possible. So, your index controller's
# index action will generate []
def generate(options, defaults={})
non_matching = @requirements.inject([]) {|a, (k, v)| ((options[k] || defaults[k]) == v) ? a : a << k}
return nil, "Options mismatch requirements: #{non_matching.join ', '}" unless non_matching.empty?
used_names = @requirements.inject({}) {|hash, (k, v)| hash[k] = true; hash}
components = @items.collect do |item|
if item.kind_of? Symbol
used_names[item] = true
value = options[item] || defaults[item] || @defaults[item]
return nil, "#{item.inspect} was not given and has no default." if value.nil? && ! (@defaults.key?(item) && @defaults[item].nil?) # Don't leave if nil value.
defaults = {} unless defaults == {} || value == defaults[item] # Stop using defaults if this component isn't the same as the default.
value
else item
end
end
@items.reverse_each do |item| # Remove default components from the end of the generated url.
break unless item.kind_of?(Symbol) && @defaults[item] == components.last
components.pop
end
# If we have any nil components then we can't proceed.
# This might need to be changed. In some cases we may be able to return all componets after nil as extras.
missing = []; components.each_with_index {|c, i| missing << @items[i] if c.nil?}
return nil, "No values provided for component#{'s' if missing.length > 1} #{missing.join ', '} but values are required due to use of later components" unless missing.empty? # how wide is your screen?
unused = (options.keys - used_names.keys).inject({}) do |unused, key|
unused[key] = options[key] if options[key] != @defaults[key]
unused
end
components.collect! {|c| c.to_s}
components.unshift(components.shift + '/') if components.length == 1 && @items.first == :controller # Add '/' to controllers
return components, unused
end
# Recognize the provided path, returning a hash of recognized values, or [nil, reason] if the path isn't recognized.
# The path should be a list of component strings.
# Options is a hash of the ?k=v pairs
def recognize(components, options={})
options = options.clone
components = components.clone
controller_class = nil
@items.each do |item|
if item == :controller # Special case for controller
if components.empty? && @defaults[:controller]
controller_class, leftover = eat_path_to_controller(@defaults[:controller].split('/'))
raise RoutingError, "Default controller does not exist: #{@defaults[:controller]}" if controller_class.nil? || leftover.empty? == false
else
controller_class, remaining_components = eat_path_to_controller(components)
return nil, "No controller found at subpath #{components.join('/')}" if controller_class.nil?
components = remaining_components
end
options[:controller] = controller_class.controller_path
elsif item.kind_of? Symbol
value = components.shift || @defaults[item]
return nil, "No value or default for parameter #{item.inspect}" if value.nil? && ! (@defaults.key?(item) && @defaults[item].nil?)
options[item] = value
else
return nil, "No value available for component #{item.inspect}" if components.empty?
component = components.shift
return nil, "Value for component #{item.inspect} doesn't match #{component}" if component != item
end
end
if controller_class.nil? && @requirements[:controller] # Load a default controller
controller_class, extras = eat_path_to_controller(@requirements[:controller].split('/'))
raise RoutingError, "Illegal controller path for route default: #{@requirements[:controller]}" unless controller_class && extras.empty?
options[:controller] = controller_class.controller_path
end
options = @requirements.merge(options)
return nil, "Route recognition didn't find a controller class!" unless controller_class
return nil, "Unused components were left: #{components.join '/'}" unless components.empty?
options.delete_if {|k, v| v.nil?} # Remove nil values.
return controller_class, options
end
def inspect
when_str = @requirements.empty? ? "" : " when #{@requirements.inspect}"
default_str = @defaults.empty? ? "" : " || #{@defaults.inspect}"
"<#{self.class.to_s} #{@items.collect{|c| c.kind_of?(String) ? c : c.inspect}.join('/').inspect}#{default_str}#{when_str}>"
end
protected
# Find the controller given a list of path components.
# Return the controller class and the unused path components.
def eat_path_to_controller(path)
path.inject([Controllers, 1]) do |(mod, length), name|
name = name.camelize
controller_name = name + "Controller"
return mod.const_get(controller_name), path[length..-1] if mod.const_available? controller_name
return nil, nil unless mod.const_available? name
[mod.const_get(name), length + 1]
end
return nil, nil # Path ended, but no controller found.
end
def items=(path)
items = path.split('/').collect {|c| (/^:(\w+)$/ =~ c) ? $1.intern : c} if path.kind_of?(String) # split and convert ':xyz' to symbols
items.shift if items.first == ""
items.pop if items.last == ""
@items = items
# Verify uniqueness of each component.
@items.inject({}) do |seen, item|
if item.kind_of? Symbol
raise ArgumentError, "Illegal route path -- duplicate item #{item}\n #{path.inspect}" if seen.key? item
seen[item] = true
end
seen
end
end
end
class RouteSet
def initialize
@routes = []
end
def add_route(route)
raise TypeError, "#{route.inspect} is not a Route instance!" unless route.kind_of?(Route)
@routes << route
end
def empty?
@routes.empty?
end
def each
@routes.each {|route| yield route}
end
# Generate a path for the provided options
# Returns the path as an array of components and a hash of unused names
# Raises RoutingError if not route can handle the provided components.
#
# Note that we don't return the first generated path. We do this so that when a route
# generates a path from a subset of the available options we can keep looking for a
# route which can generate a path that uses more options.
# Note that we *do* return immediately if
def generate(options, request)
raise RoutingError, "There are no routes defined!" if @routes.empty?
options = options.symbolize_keys
defaults = request.path_parameters.symbolize_keys
expand_controller_path!(options, defaults)
failures = []
selected = nil
self.each do |route|
path, unused = route.generate(options, defaults)
if path.nil?
failures << [route, unused] if ActionController::Base.debug_routes
else
return path, unused if unused.empty? # Found a perfect route -- we're finished.
if selected.nil? || unused.length < selected.last.length
failures << [selected.first, "A better url than #{selected[1]} was found."] if selected
selected = [route, path, unused]
end
end
end
return selected[1..-1] unless selected.nil?
raise RoutingError.new("Generation failure: No route for url_options #{options.inspect}, defaults: #{defaults.inspect}", failures)
end
# Recognize the provided path.
# Raise RoutingError if the path can't be recognized.
def recognize!(request)
path = ((%r{^/?(.*)/?$} =~ request.path) ? $1 : request.path).split('/')
raise RoutingError, "There are no routes defined!" if @routes.empty?
failures = []
self.each do |route|
controller, options = route.recognize(path)
if controller.nil?
failures << [route, options] if ActionController::Base.debug_routes
else
options.each {|k, v| request.path_parameters[k] = CGI.unescape(v)}
return controller
end
end
raise RoutingError.new("No route for path: #{path.join('/').inspect}", failures)
end
def expand_controller_path!(options, defaults)
if options[:controller]
if /^\// =~ options[:controller]
options[:controller] = options[:controller][1..-1]
defaults.clear # Sending to absolute controller implies fresh defaults
else
relative_to = defaults[:controller] ? defaults[:controller].split('/')[0..-2].join('/') : ''
options[:controller] = relative_to.empty? ? options[:controller] : "#{relative_to}/#{options[:controller]}"
defaults.delete(:action) if options.key?(:controller)
end
else
options[:controller] = defaults[:controller]
end
end
def route(*args)
add_route(Route.new(*args))
end
alias :connect :route
def reload
begin
require_dependency(ROUTE_FILE)
rescue NoMethodError
# ignore for unit tests
rescue LoadError, ScriptError => e
raise RoutingError, "Cannot load config/routes.rb:\n #{e.message}"
ensure # Ensure that there is at least one route:
connect(':controller/:action/:id', :action => 'index', :id => nil) if @routes.empty?
end
end
def draw
@routes.clear
yield self
end
end
def self.draw(*args, &block)
Routes.draw(*args) {|*args| block.call(*args)}
end
Routes = RouteSet.new
Routes.reload # Server will die on load if SyntaxError
end
end