mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Improved performance of Routes generation by a factor of 5 #1434 [Nicholas Seckar] Added named routes (NEEDS BETTER DESCRIPTION) #1434 [Nicholas Seckar]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1496 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
540d005ca5
commit
8e56f5ea3e
15 changed files with 1487 additions and 839 deletions
|
@ -1,8 +1,12 @@
|
||||||
*SVN*
|
*SVN*
|
||||||
|
|
||||||
* Improved AbstractRequest documentation. #1483 [court3nay@gmail.com]
|
* Improved performance of Routes generation by a factor of 5 #1434 [Nicholas Seckar]
|
||||||
|
|
||||||
* Added ActionController::Base.allow_concurrency to control whether the application is thread-safe, so multi-threaded servers like WEBrick knows whether to apply a mutex around the performance of each action. Action Pack and Active Record are by default thread-safe, but many applications may not be. Turned off by default.
|
* Added named routes (NEEDS BETTER DESCRIPTION) #1434 [Nicholas Seckar]
|
||||||
|
|
||||||
|
* Improved AbstractRequest documentation #1483 [court3nay@gmail.com]
|
||||||
|
|
||||||
|
* Added ActionController::Base.allow_concurrency to control whether the application is thread-safe, so multi-threaded servers like WEBrick knows whether to apply a mutex around the performance of each action. Turned off by default. EXPERIMENTAL FEATURE.
|
||||||
|
|
||||||
* Added TextHelper#word_wrap(text, line_length = 80) #1449 [tuxie@dekadance.se]
|
* Added TextHelper#word_wrap(text, line_length = 80) #1449 [tuxie@dekadance.se]
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
require 'action_controller/request'
|
require 'action_controller/request'
|
||||||
require 'action_controller/response'
|
require 'action_controller/response'
|
||||||
require 'action_controller/routing'
|
require 'action_controller/routing'
|
||||||
|
require 'action_controller/code_generation'
|
||||||
require 'action_controller/url_rewriter'
|
require 'action_controller/url_rewriter'
|
||||||
require 'drb'
|
require 'drb'
|
||||||
|
|
||||||
|
@ -11,7 +12,7 @@ module ActionController #:nodoc:
|
||||||
end
|
end
|
||||||
class MissingTemplate < ActionControllerError #:nodoc:
|
class MissingTemplate < ActionControllerError #:nodoc:
|
||||||
end
|
end
|
||||||
class RoutingError < ActionControllerError#:nodoc:
|
class RoutingError < ActionControllerError #:nodoc:
|
||||||
attr_reader :failures
|
attr_reader :failures
|
||||||
def initialize(message, failures=[])
|
def initialize(message, failures=[])
|
||||||
super(message)
|
super(message)
|
||||||
|
|
235
actionpack/lib/action_controller/code_generation.rb
Normal file
235
actionpack/lib/action_controller/code_generation.rb
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
module ActionController
|
||||||
|
module CodeGeneration #:nodoc
|
||||||
|
class GenerationError < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
|
class Source
|
||||||
|
attr_reader :lines, :indentation_level
|
||||||
|
IndentationString = ' '
|
||||||
|
def initialize
|
||||||
|
@lines, @indentation_level = [], 0
|
||||||
|
end
|
||||||
|
def line(line)
|
||||||
|
@lines << (IndentationString * @indentation_level + line)
|
||||||
|
end
|
||||||
|
alias :<< :line
|
||||||
|
|
||||||
|
def indent
|
||||||
|
@indentation_level += 1
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
@indentation_level -= 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s() lines.join("\n") end
|
||||||
|
end
|
||||||
|
|
||||||
|
class CodeGenerator
|
||||||
|
attr_accessor :source, :locals
|
||||||
|
def initialize(source = nil)
|
||||||
|
@locals = []
|
||||||
|
@source = source || Source.new
|
||||||
|
end
|
||||||
|
|
||||||
|
BeginKeywords = %w(if unless begin until while def).collect {|kw| kw.to_sym}
|
||||||
|
ResumeKeywords = %w(elsif else rescue).collect {|kw| kw.to_sym}
|
||||||
|
Keywords = BeginKeywords + ResumeKeywords
|
||||||
|
|
||||||
|
def method_missing(keyword, *text)
|
||||||
|
if Keywords.include? keyword
|
||||||
|
if ResumeKeywords.include? keyword
|
||||||
|
raise GenerationError, "Can only resume with #{keyword} immediately after an end" unless source.lines.last =~ /^\s*end\s*$/
|
||||||
|
source.lines.pop # Remove the 'end'
|
||||||
|
end
|
||||||
|
|
||||||
|
line "#{keyword} #{text.join ' '}"
|
||||||
|
begin source.indent { yield(self.dup) }
|
||||||
|
ensure line 'end'
|
||||||
|
end
|
||||||
|
else
|
||||||
|
super(keyword, *text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def line(*args) self.source.line(*args) end
|
||||||
|
alias :<< :line
|
||||||
|
def indent(*args, &block) source(*args, &block) end
|
||||||
|
def to_s() source.to_s end
|
||||||
|
|
||||||
|
def share_locals_with(other)
|
||||||
|
other.locals = self.locals = (other.locals | locals)
|
||||||
|
end
|
||||||
|
|
||||||
|
FieldsToDuplicate = [:locals]
|
||||||
|
def dup
|
||||||
|
copy = self.class.new(source)
|
||||||
|
self.class::FieldsToDuplicate.each do |sym|
|
||||||
|
value = self.send(sym)
|
||||||
|
value = value.dup unless value.nil? || value.is_a?(Numeric)
|
||||||
|
copy.send("#{sym}=", value)
|
||||||
|
end
|
||||||
|
return copy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class RecognitionGenerator < CodeGenerator
|
||||||
|
Attributes = [:after, :before, :current, :results, :constants, :depth, :move_ahead, :finish_statement]
|
||||||
|
attr_accessor(*Attributes)
|
||||||
|
FieldsToDuplicate = CodeGenerator::FieldsToDuplicate + Attributes
|
||||||
|
|
||||||
|
def initialize(*args)
|
||||||
|
super(*args)
|
||||||
|
@after, @before = [], []
|
||||||
|
@current = nil
|
||||||
|
@results, @constants = {}, {}
|
||||||
|
@depth = 0
|
||||||
|
@move_ahead = nil
|
||||||
|
@finish_statement = Proc.new {|hash_expr| hash_expr}
|
||||||
|
end
|
||||||
|
|
||||||
|
def if_next_matches(string, &block)
|
||||||
|
test = Routing.test_condition(next_segment(true), string)
|
||||||
|
self.if(test, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def move_forward(places = 1)
|
||||||
|
dup = self.dup
|
||||||
|
dup.depth += 1
|
||||||
|
dup.move_ahead = places
|
||||||
|
yield dup
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_segment(assign_inline = false, default = nil)
|
||||||
|
if locals.include?(segment_name)
|
||||||
|
code = segment_name
|
||||||
|
else
|
||||||
|
code = "#{segment_name} = #{path_name}[#{index_name}]"
|
||||||
|
if assign_inline
|
||||||
|
code = "(#{code})"
|
||||||
|
else
|
||||||
|
line(code)
|
||||||
|
code = segment_name
|
||||||
|
end
|
||||||
|
|
||||||
|
locals << segment_name
|
||||||
|
end
|
||||||
|
code = "(#{code} || #{default.inspect})" if default
|
||||||
|
|
||||||
|
return code
|
||||||
|
end
|
||||||
|
|
||||||
|
def segment_name() "segment#{depth}".to_sym end
|
||||||
|
def path_name() :path end
|
||||||
|
def index_name
|
||||||
|
move_ahead, @move_ahead = @move_ahead, nil
|
||||||
|
move_ahead ? "index += #{move_ahead}" : 'index'
|
||||||
|
end
|
||||||
|
|
||||||
|
def continue
|
||||||
|
dup = self.dup
|
||||||
|
dup.before << dup.current
|
||||||
|
dup.current = dup.after.shift
|
||||||
|
dup.go
|
||||||
|
end
|
||||||
|
|
||||||
|
def go
|
||||||
|
if current then current.write_recognition(self)
|
||||||
|
else self.finish
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def result(key, expression, delay = false)
|
||||||
|
unless delay
|
||||||
|
line "#{key}_value = #{expression}"
|
||||||
|
expression = "#{key}_value"
|
||||||
|
end
|
||||||
|
results[key] = expression
|
||||||
|
end
|
||||||
|
def constant_result(key, object)
|
||||||
|
constants[key] = object
|
||||||
|
end
|
||||||
|
|
||||||
|
def finish(ensure_traversal_finished = true)
|
||||||
|
pairs = []
|
||||||
|
(results.keys + constants.keys).uniq.each do |key|
|
||||||
|
pairs << "#{key.to_s.inspect} => #{results[key] ? results[key] : constants[key].inspect}"
|
||||||
|
end
|
||||||
|
hash_expr = "{#{pairs.join(', ')}}"
|
||||||
|
|
||||||
|
statement = finish_statement.call(hash_expr)
|
||||||
|
if ensure_traversal_finished then self.if("! #{next_segment(true)}") {|gp| gp << statement}
|
||||||
|
else self << statement
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class GenerationGenerator < CodeGenerator
|
||||||
|
Attributes = [:after, :before, :current, :segments]
|
||||||
|
attr_accessor(*Attributes)
|
||||||
|
FieldsToDuplicate = CodeGenerator::FieldsToDuplicate + Attributes
|
||||||
|
|
||||||
|
def initialize(*args)
|
||||||
|
super(*args)
|
||||||
|
@after, @before = [], []
|
||||||
|
@current = nil
|
||||||
|
@segments = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def hash_name() 'hash' end
|
||||||
|
def local_name(key) "#{key}_value" end
|
||||||
|
|
||||||
|
def hash_value(key, assign = true, default = nil)
|
||||||
|
if locals.include?(local_name(key)) then code = local_name(key)
|
||||||
|
else
|
||||||
|
code = "hash[#{key.to_sym.inspect}]"
|
||||||
|
if assign
|
||||||
|
code = "(#{local_name(key)} = #{code})"
|
||||||
|
locals << local_name(key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
code = "(#{code} || #{default.inspect})" if default
|
||||||
|
return code
|
||||||
|
end
|
||||||
|
|
||||||
|
def expire_for_keys(*keys)
|
||||||
|
return if keys.empty?
|
||||||
|
conds = keys.collect {|key| "expire_on[#{key.to_sym.inspect}]"}
|
||||||
|
line "not_expired, #{hash_name} = false, options if not_expired && #{conds.join(' && ')}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_segment(*segments)
|
||||||
|
d = dup
|
||||||
|
d.segments.concat segments
|
||||||
|
yield d
|
||||||
|
end
|
||||||
|
|
||||||
|
def go
|
||||||
|
if current then current.write_generation(self)
|
||||||
|
else self.finish
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def continue
|
||||||
|
d = dup
|
||||||
|
d.before << d.current
|
||||||
|
d.current = d.after.shift
|
||||||
|
d.go
|
||||||
|
end
|
||||||
|
|
||||||
|
def finish
|
||||||
|
line %("#{segments.join('/')}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_conditions(conditions)
|
||||||
|
tests = []
|
||||||
|
generator = nil
|
||||||
|
conditions.each do |key, condition|
|
||||||
|
tests << (generator || self).hash_value(key, true) if condition.is_a? Regexp
|
||||||
|
tests << Routing.test_condition((generator || self).hash_value(key, false), condition)
|
||||||
|
generator = self.dup unless generator
|
||||||
|
end
|
||||||
|
return tests.join(' && ')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -38,7 +38,6 @@ module ActionController
|
||||||
method == :head
|
method == :head
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# Determine whether the body of a POST request is URL-encoded (default),
|
# Determine whether the body of a POST request is URL-encoded (default),
|
||||||
# XML, or YAML by checking the Content-Type HTTP header:
|
# XML, or YAML by checking the Content-Type HTTP header:
|
||||||
#
|
#
|
||||||
|
@ -78,9 +77,9 @@ module ActionController
|
||||||
post_format == :yaml && post?
|
post_format == :yaml && post?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Is the X-Requested-With HTTP header present and does it contain the
|
# Returns true if the request's "X-Requested-With" header contains
|
||||||
# string "XMLHttpRequest"?. The Prototype Javascript library sends this
|
# "XMLHttpRequest". (The Prototype Javascript library sends this header with
|
||||||
# header with every Ajax request.
|
# every Ajax request.)
|
||||||
def xml_http_request?
|
def xml_http_request?
|
||||||
not /XMLHttpRequest/i.match(env['HTTP_X_REQUESTED_WITH']).nil?
|
not /XMLHttpRequest/i.match(env['HTTP_X_REQUESTED_WITH']).nil?
|
||||||
end
|
end
|
||||||
|
@ -186,7 +185,11 @@ module ActionController
|
||||||
|
|
||||||
def path_parameters=(parameters)
|
def path_parameters=(parameters)
|
||||||
@path_parameters = parameters
|
@path_parameters = parameters
|
||||||
@parameters = nil
|
@symbolized_path_parameters = @parameters = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def symbolized_path_parameters
|
||||||
|
@symbolized_path_parameters ||= path_parameters.symbolize_keys
|
||||||
end
|
end
|
||||||
|
|
||||||
def path_parameters
|
def path_parameters
|
||||||
|
|
|
@ -1,342 +1,578 @@
|
||||||
module ActionController
|
module ActionController
|
||||||
# See http://manuals.rubyonrails.com/read/chapter/65
|
|
||||||
module Routing
|
module Routing
|
||||||
class Route #:nodoc:
|
class << self
|
||||||
attr_reader :defaults # The defaults hash
|
|
||||||
|
def expiry_hash(options, recall)
|
||||||
def initialize(path, hash={})
|
k = v = nil
|
||||||
raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash)
|
expire_on = {}
|
||||||
@defaults = hash[:defaults].kind_of?(Hash) ? hash.delete(:defaults) : {}
|
options.each {|k, v| expire_on[k] = ((rcv = recall[k]) && (rcv != v))}
|
||||||
@requirements = hash[:requirements].kind_of?(Hash) ? hash.delete(:requirements) : {}
|
expire_on
|
||||||
self.items = path
|
end
|
||||||
|
|
||||||
|
def extract_parameter_value(parameter) #:nodoc:
|
||||||
|
CGI.escape((parameter.respond_to?(:to_param) ? parameter.to_param : parameter).to_s)
|
||||||
|
end
|
||||||
|
def controller_relative_to(controller, previous)
|
||||||
|
if controller.nil? then previous
|
||||||
|
elsif controller[0] == ?/ then controller[1..-1]
|
||||||
|
elsif %r{^(.*)/} =~ previous then "#{$1}/#{controller}"
|
||||||
|
else controller
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def treat_hash(hash)
|
||||||
|
k = v = nil
|
||||||
hash.each do |k, v|
|
hash.each do |k, v|
|
||||||
raise TypeError, "Hash keys must be symbols!" unless k.kind_of? Symbol
|
hash[k] = (v.respond_to? :to_param) ? v.to_param.to_s : v.to_s
|
||||||
if v.kind_of? Regexp
|
end
|
||||||
raise ArgumentError, "Regexp requirement on #{k}, but #{k} is not in this route's path!" unless @items.include? k
|
hash
|
||||||
@requirements[k] = v
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class RoutingError < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def test_condition(expression, condition)
|
||||||
|
case condition
|
||||||
|
when String then "(#{expression} == #{condition.inspect})"
|
||||||
|
when Regexp then
|
||||||
|
condition = Regexp.new("^#{condition.source}$") unless /^\^.*\$$/ =~ condition.source
|
||||||
|
"(#{condition.inspect} =~ #{expression})"
|
||||||
|
when true then expression
|
||||||
|
when nil then "! #{expression}"
|
||||||
else
|
else
|
||||||
(@items.include?(k) ? @defaults : @requirements)[k] = (v.nil? ? nil : v.to_s)
|
raise ArgumentError, "Valid criteria are strings, regular expressions, true, or nil"
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@defaults.each do |k, v|
|
|
||||||
raise ArgumentError, "A default has been specified for #{k}, but #{k} is not in the path!" unless @items.include? k
|
|
||||||
@defaults[k] = v.to_s unless v.kind_of?(String) || v.nil?
|
|
||||||
end
|
|
||||||
@requirements.each {|k, v| raise ArgumentError, "A Regexp requirement has been specified for #{k}, but #{k} is not in the path!" if v.kind_of?(Regexp) && ! @items.include?(k)}
|
|
||||||
|
|
||||||
# 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
|
||||||
end
|
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.keys.select {|name| ! passes_requirements?(name, options[name] || defaults[name])}
|
|
||||||
non_matching.collect! {|name| requirements_for(name)}
|
|
||||||
return nil, "Mismatching option#{'s' if non_matching.length > 1}:\n #{non_matching.join '\n '}" unless non_matching.empty?
|
|
||||||
|
|
||||||
used_names = @requirements.inject({}) {|hash, (k, v)| hash[k] = true; hash} # Mark requirements as used so they don't get put in the query params
|
|
||||||
components = @items.collect do |item|
|
|
||||||
|
|
||||||
if item.kind_of? Symbol
|
class Component #:nodoc
|
||||||
collection = false
|
def dynamic?() false end
|
||||||
|
def optional?() false end
|
||||||
|
|
||||||
if /^\*/ =~ item.to_s
|
def key() nil end
|
||||||
collection = true
|
|
||||||
item = item.to_s.sub(/^\*/,"").intern
|
def self.new(string, *args)
|
||||||
end
|
return super(string, *args) unless self == Component
|
||||||
|
case string
|
||||||
|
when ':controller' then ControllerComponent.new(:controller, *args)
|
||||||
|
when /^:(\w+)$/ then DynamicComponent.new($1, *args)
|
||||||
|
when /^\*(\w+)$/ then PathComponent.new($1, *args)
|
||||||
|
else StaticComponent.new(string, *args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
used_names[item] = true
|
class StaticComponent < Component #:nodoc
|
||||||
value = options[item] || defaults[item] || @defaults[item]
|
attr_reader :value
|
||||||
return nil, requirements_for(item) unless passes_requirements?(item, value)
|
|
||||||
|
def initialize(value)
|
||||||
|
@value = value
|
||||||
|
end
|
||||||
|
|
||||||
defaults = {} unless defaults == {} || value == defaults[item] # Stop using defaults if this component isn't the same as the default.
|
def write_recognition(g)
|
||||||
|
g.if_next_matches(value) do |gp|
|
||||||
|
gp.move_forward {|gpp| gpp.continue}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if value.nil? || item == :controller
|
def write_generation(g)
|
||||||
value
|
g.add_segment(value) {|gp| gp.continue }
|
||||||
elsif collection
|
end
|
||||||
if value.kind_of?(Array)
|
end
|
||||||
value = value.collect {|v| Routing.extract_parameter_value(v)}.join('/')
|
|
||||||
else
|
class DynamicComponent < Component #:nodoc
|
||||||
value = Routing.extract_parameter_value(value).gsub(/%2F/, "/")
|
attr_reader :key, :default
|
||||||
end
|
attr_accessor :condition
|
||||||
value
|
|
||||||
else
|
def dynamic?() true end
|
||||||
Routing.extract_parameter_value(value)
|
def optional?() @optional end
|
||||||
end
|
|
||||||
else
|
def default=(default)
|
||||||
item
|
@optional = true
|
||||||
|
@default = default
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(key, options = {})
|
||||||
|
@key = key.to_sym
|
||||||
|
@default, @condition = options[:default], options[:condition]
|
||||||
|
@optional = options.key?(:default)
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_check(g)
|
||||||
|
presence = "#{g.hash_value(key, !! default)}"
|
||||||
|
if default
|
||||||
|
"!(#{presence} && #{g.hash_value(key, false)} != #{default.inspect})"
|
||||||
|
else
|
||||||
|
"! #{presence}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_generation(g)
|
||||||
|
wrote_dropout = write_dropout_generation(g)
|
||||||
|
write_continue_generation(g, wrote_dropout)
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_dropout_generation(g)
|
||||||
|
return false unless optional? && g.after.all? {|c| c.optional?}
|
||||||
|
|
||||||
|
check = [default_check(g)]
|
||||||
|
gp = g.dup # Use another generator to write the conditions after the first &&
|
||||||
|
# We do this to ensure that the generator will not assume x_value is set. It will
|
||||||
|
# not be set if it follows a false condition -- for example, false && (x = 2)
|
||||||
|
|
||||||
|
gp.after.map {|c| c.default_check gp}
|
||||||
|
gp.if(check.join(' && ')) { gp.finish } # If this condition is met, we stop here
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_continue_generation(g, use_else)
|
||||||
|
test = Routing.test_condition(g.hash_value(key, true, default), condition || true)
|
||||||
|
check = (use_else && condition.nil? && default) ? [:else] : [use_else ? :elsif : :if, test]
|
||||||
|
|
||||||
|
g.send(*check) do |gp|
|
||||||
|
gp.expire_for_keys(key) unless gp.after.empty?
|
||||||
|
add_segments_to(gp) {|gpp| gpp.continue}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_segments_to(g)
|
||||||
|
g.add_segment(%(\#{CGI.escape(#{g.hash_value(key, true, default)})})) {|gp| yield gp}
|
||||||
|
end
|
||||||
|
|
||||||
|
def recognition_check(g)
|
||||||
|
test_type = [true, nil].include?(condition) ? :presence : :constraint
|
||||||
|
|
||||||
|
prefix = condition.is_a?(Regexp) ? "#{g.next_segment(true)} && " : ''
|
||||||
|
check = prefix + Routing.test_condition(g.next_segment(true), condition || true)
|
||||||
|
|
||||||
|
g.if(check) {|gp| yield gp, test_type}
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_recognition(g)
|
||||||
|
test_type = nil
|
||||||
|
recognition_check(g) do |gp, test_type|
|
||||||
|
assign_result(gp) {|gpp| gpp.continue}
|
||||||
|
end
|
||||||
|
|
||||||
|
if optional? && g.after.all? {|c| c.optional?}
|
||||||
|
call = (test_type == :presence) ? [:else] : [:elsif, "! #{g.next_segment(true)}"]
|
||||||
|
|
||||||
|
g.send(*call) do |gp|
|
||||||
|
assign_default(gp)
|
||||||
|
gp.after.each {|c| c.assign_default(gp)}
|
||||||
|
gp.finish(false)
|
||||||
end
|
end
|
||||||
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}
|
|
||||||
return components, unused
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def assign_result(g, with_default = false)
|
||||||
|
g.result key, "CGI.unescape(#{g.next_segment(true, with_default ? default : nil)})"
|
||||||
|
g.move_forward {|gp| yield gp}
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_default(g)
|
||||||
|
g.constant_result key, default unless default.nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ControllerComponent < DynamicComponent #:nodoc
|
||||||
|
def key() :controller end
|
||||||
|
|
||||||
|
def add_segments_to(g)
|
||||||
|
g.add_segment(%(\#{#{g.hash_value(key, true, default)}})) {|gp| yield gp}
|
||||||
|
end
|
||||||
|
|
||||||
|
def recognition_check(g)
|
||||||
|
g << "controller_result = ::ActionController::Routing::ControllerComponent.traverse_to_controller(#{g.path_name}, #{g.index_name})"
|
||||||
|
g.if('controller_result') do |gp|
|
||||||
|
gp << 'controller_value, segments_to_controller = controller_result'
|
||||||
|
gp.move_forward('segments_to_controller') {|gpp| yield gpp, :constraint}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_result(g)
|
||||||
|
g.result key, 'controller_value'
|
||||||
|
yield g
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_default(g)
|
||||||
|
ControllerComponent.assign_controller(g, default)
|
||||||
|
end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def assign_controller(g, controller)
|
||||||
|
expr = "::Controllers::#{controller.split('/').collect {|c| c.camelize}.join('::')}Controller"
|
||||||
|
g.result :controller, expr, true
|
||||||
|
end
|
||||||
|
|
||||||
|
def traverse_to_controller(segments, start_at = 0)
|
||||||
|
mod = ::Controllers
|
||||||
|
length = segments.length
|
||||||
|
index = start_at
|
||||||
|
mod_name = controller_name = segment = nil
|
||||||
|
|
||||||
# Recognize the provided path, returning a hash of recognized values, or [nil, reason] if the path isn't recognized.
|
while index < length
|
||||||
# The path should be a list of component strings.
|
return nil unless /^[a-z][a-z\d_]*$/ =~ (segment = segments[index])
|
||||||
# Options is a hash of the ?k=v pairs
|
index += 1
|
||||||
def recognize(components, options={})
|
|
||||||
options = options.clone
|
|
||||||
components = components.clone
|
|
||||||
controller_class = nil
|
|
||||||
|
|
||||||
@items.each do |item|
|
mod_name = segment.camelize
|
||||||
if item == :controller # Special case for controller
|
controller_name = "#{mod_name}Controller"
|
||||||
if components.empty? && @defaults[:controller]
|
|
||||||
controller_class, leftover = eat_path_to_controller(@defaults[:controller].split('/'))
|
return eval("mod::#{controller_name}"), (index - start_at) if mod.const_available?(controller_name)
|
||||||
raise RoutingError, "Default controller does not exist: #{@defaults[:controller]}" if controller_class.nil? || leftover.empty? == false
|
return nil unless mod.const_available?(mod_name)
|
||||||
else
|
mod = eval("mod::#{mod_name}")
|
||||||
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
|
|
||||||
return nil, requirements_for(:controller) unless passes_requirements?(:controller, options[:controller])
|
|
||||||
elsif /^\*/ =~ item.to_s
|
|
||||||
if components.empty?
|
|
||||||
value = @defaults.has_key?(item) ? @defaults[item].clone : []
|
|
||||||
else
|
|
||||||
value = components.clone
|
|
||||||
end
|
|
||||||
value.collect! {|c| CGI.unescape c}
|
|
||||||
components = []
|
|
||||||
def value.to_s() self.join('/') end
|
|
||||||
options[item.to_s.sub(/^\*/,"").intern] = value
|
|
||||||
elsif item.kind_of? Symbol
|
|
||||||
value = components.shift || @defaults[item]
|
|
||||||
return nil, requirements_for(item) unless passes_requirements?(item, value)
|
|
||||||
options[item] = value.nil? ? value : CGI.unescape(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
|
||||||
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
|
|
||||||
@requirements.each {|k,v| options[k] ||= v unless v.kind_of?(Regexp)}
|
|
||||||
|
|
||||||
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
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class PathComponent < DynamicComponent #:nodoc
|
||||||
|
def optional?() true end
|
||||||
|
def default() '' end
|
||||||
|
def condition() nil end
|
||||||
|
|
||||||
|
def write_generation(g)
|
||||||
|
raise RoutingError, 'Path components must occur last' unless g.after.empty?
|
||||||
|
g.if("#{g.hash_value(key, true)} && ! #{g.hash_value(key, true)}.empty?") do
|
||||||
|
g << "#{g.hash_value(key, true)} = #{g.hash_value(key, true)}.join('/') unless #{g.hash_value(key, true)}.is_a?(String)"
|
||||||
|
g.add_segment("\#{CGI.escape_skipping_slashes(#{g.hash_value(key, true)})}") {|gp| gp.finish }
|
||||||
|
end
|
||||||
|
g.else { g.finish }
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_recognition(g)
|
||||||
|
raise RoutingError, "Path components must occur last" unless g.after.empty?
|
||||||
|
|
||||||
|
start = g.index_name
|
||||||
|
start = "(#{start})" unless /^\w+$/ =~ start
|
||||||
|
|
||||||
|
value_expr = "#{g.path_name}[#{start}..-1] || []"
|
||||||
|
g.result key, "ActionController::Routing::PathComponent::Result.new(#{value_expr})"
|
||||||
|
g.finish(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
class Result < ::Array
|
||||||
|
def to_s() join '/' end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Route
|
||||||
|
attr_accessor :components, :known
|
||||||
|
attr_reader :path, :options, :keys
|
||||||
|
|
||||||
|
def initialize(path, options = {})
|
||||||
|
@path, @options = path, options
|
||||||
|
|
||||||
|
initialize_components path
|
||||||
|
defaults, conditions = initialize_hashes options.dup
|
||||||
|
configure_components(defaults, conditions)
|
||||||
|
initialize_keys
|
||||||
|
end
|
||||||
|
|
||||||
def inspect
|
def inspect
|
||||||
when_str = @requirements.empty? ? "" : " when #{@requirements.inspect}"
|
"<#{self.class} #{path.inspect}, #{options.inspect[1..-1]}>"
|
||||||
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
|
end
|
||||||
|
|
||||||
|
def write_generation(generator = CodeGeneration::GenerationGenerator.new)
|
||||||
|
generator.before, generator.current, generator.after = [], components.first, (components[1..-1] || [])
|
||||||
|
|
||||||
|
if known.empty? then generator.go
|
||||||
|
else generator.if(generator.check_conditions(known)) {|gp| gp.go }
|
||||||
|
end
|
||||||
|
|
||||||
|
generator
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_recognition(generator = CodeGeneration::RecognitionGenerator.new)
|
||||||
|
g = generator.dup
|
||||||
|
g.share_locals_with generator
|
||||||
|
g.before, g.current, g.after = [], components.first, (components[1..-1] || [])
|
||||||
|
|
||||||
|
known.each do |key, value|
|
||||||
|
if key == :controller then ControllerComponent.assign_controller(g, value)
|
||||||
|
else g.constant_result(key, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
g.go
|
||||||
|
|
||||||
|
generator
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_keys
|
||||||
|
@keys = (components.collect {|c| c.key} + known.keys).compact
|
||||||
|
@keys.freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
def extra_keys(options)
|
||||||
|
options.keys - @keys
|
||||||
|
end
|
||||||
|
|
||||||
|
def matches_controller?(controller)
|
||||||
|
if known[:controller] then known[:controller] == controller
|
||||||
|
else
|
||||||
|
c = components.find {|c| c.key == :controller}
|
||||||
|
return false unless c
|
||||||
|
return c.condition.nil? || eval(Routing.test_condition('controller', c.condition))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
# Find the controller given a list of path components.
|
|
||||||
# Return the controller class and the unused path components.
|
def initialize_components(path)
|
||||||
def eat_path_to_controller(path)
|
path = path.split('/') if path.is_a? String
|
||||||
path.inject([Controllers, 1]) do |(mod, length), name|
|
self.components = path.collect {|str| Component.new str}
|
||||||
name = name.camelize
|
|
||||||
return nil, nil unless /^[A-Z][_a-zA-Z\d]*$/ =~ name
|
|
||||||
controller_name = name + "Controller"
|
|
||||||
return eval("mod::#{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
|
end
|
||||||
|
|
||||||
|
def initialize_hashes(options)
|
||||||
|
path_keys = components.collect {|c| c.key }.compact
|
||||||
|
self.known = {}
|
||||||
|
defaults = options.delete(:defaults) || {}
|
||||||
|
conditions = options.delete(:require) || {}
|
||||||
|
conditions.update(options.delete(:requirements) || {})
|
||||||
|
|
||||||
def items=(path)
|
options.each do |k, v|
|
||||||
items = path.split('/').collect {|c| (/^(:|\*)(\w+)$/ =~ c) ? (($1 == ':' ) ? $2.intern : "*#{$2}".intern) : c} if path.kind_of?(String) # split and convert ':xyz' to symbols
|
if path_keys.include?(k) then (v.is_a?(Regexp) ? conditions : defaults)[k] = v
|
||||||
items.shift if items.first == ""
|
else known[k] = v
|
||||||
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
|
end
|
||||||
seen
|
|
||||||
end
|
end
|
||||||
|
[defaults, conditions]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def configure_components(defaults, conditions)
|
||||||
|
components.each do |component|
|
||||||
|
if defaults.key?(component.key) then component.default = defaults[component.key]
|
||||||
|
elsif component.key == :action then component.default = 'index'
|
||||||
|
elsif component.key == :id then component.default = nil
|
||||||
|
end
|
||||||
|
|
||||||
# Verify that the given value passes this route's requirements
|
component.condition = conditions[component.key] if conditions.key?(component.key)
|
||||||
def passes_requirements?(name, value)
|
|
||||||
return @defaults.key?(name) && @defaults[name].nil? if value.nil? # Make sure it's there if it should be
|
|
||||||
|
|
||||||
case @requirements[name]
|
|
||||||
when nil then true
|
|
||||||
when Regexp then
|
|
||||||
value = value.to_s
|
|
||||||
match = @requirements[name].match(value)
|
|
||||||
match && match[0].length == value.length
|
|
||||||
else
|
|
||||||
@requirements[name] == value.to_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
def requirements_for(name)
|
|
||||||
name = name.to_s.sub(/^\*/,"").intern if (/^\*/ =~ name.inspect)
|
|
||||||
presence = (@defaults.key?(name) && @defaults[name].nil?)
|
|
||||||
requirement = case @requirements[name]
|
|
||||||
when nil then nil
|
|
||||||
when Regexp then "match #{@requirements[name].inspect}"
|
|
||||||
else "be equal to #{@requirements[name].inspect}"
|
|
||||||
end
|
|
||||||
if presence && requirement then "#{name} must be present and #{requirement}"
|
|
||||||
elsif presence || requirement then "#{name} must #{requirement || 'be present'}"
|
|
||||||
else "#{name} has no requirements"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class RouteSet#:nodoc:
|
class RouteSet
|
||||||
|
attr_reader :routes, :categories, :controller_to_selector
|
||||||
def initialize
|
def initialize
|
||||||
@routes = []
|
@routes = []
|
||||||
|
@generation_methods = Hash.new(:generate_default_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_route(route)
|
def generate(options, request_or_recall_hash = {})
|
||||||
raise TypeError, "#{route.inspect} is not a Route instance!" unless route.kind_of?(Route)
|
recall = request_or_recall_hash.is_a?(Hash) ? request_or_recall_hash : request_or_recall_hash.symbolized_path_parameters
|
||||||
@routes << route
|
|
||||||
end
|
if ((rc_c = recall[:controller]) && rc_c.include?(?/)) || ((c = options[:controller]) && c.include?(?/))
|
||||||
def empty?
|
options[:controller] = Routing.controller_relative_to(c, rc_c)
|
||||||
@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
|
|
||||||
if options.empty? then options = defaults.clone # Get back the current url if no options was passed
|
|
||||||
else expand_controller_path!(options, defaults) # Expand the supplied controller path.
|
|
||||||
end
|
end
|
||||||
defaults.delete_if {|k, v| options.key?(k) && options[k].nil?} # Remove defaults that have been manually cleared using :name => nil
|
options = recall.dup if options.empty? # XXX move to url_rewriter?
|
||||||
|
Routing.treat_hash(options) # XXX Move inwards (to generated code) or inline?
|
||||||
|
merged = recall.merge(options)
|
||||||
|
expire_on = Routing.expiry_hash(options, recall)
|
||||||
|
|
||||||
|
path, keys = generate_path(merged, options, expire_on)
|
||||||
|
|
||||||
|
# Factor out?
|
||||||
|
extras = {}
|
||||||
|
k = nil
|
||||||
|
keys.each {|k| extras[k] = options[k]}
|
||||||
|
[path, extras]
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_path(merged, options, expire_on)
|
||||||
|
send @generation_methods[merged[:controller]], merged, options, expire_on
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_generation
|
||||||
|
@generation_methods = Hash.new(:generate_default_path)
|
||||||
|
categorize_routes.each do |controller, routes|
|
||||||
|
next unless routes.length < @routes.length
|
||||||
|
|
||||||
|
ivar = controller.gsub('/', '__')
|
||||||
|
method_name = "generate_path_for_#{ivar}".to_sym
|
||||||
|
instance_variable_set "@#{ivar}", routes
|
||||||
|
code = generation_code_for(ivar, method_name).to_s
|
||||||
|
eval(code)
|
||||||
|
|
||||||
|
@generation_methods[controller.to_s] = method_name
|
||||||
|
@generation_methods[controller.to_sym] = method_name
|
||||||
|
end
|
||||||
|
|
||||||
|
eval(generation_code_for('routes', 'generate_default_path').to_s)
|
||||||
|
end
|
||||||
|
|
||||||
failures = []
|
def recognize(request)
|
||||||
selected = nil
|
string_path = request.path
|
||||||
self.each do |route|
|
string_path.chomp! if string_path[0] == ?/
|
||||||
path, unused = route.generate(options, defaults)
|
path = string_path.split '/'
|
||||||
if path.nil?
|
path.shift
|
||||||
failures << [route, unused] if ActionController::Base.debug_routes
|
|
||||||
else
|
hash = recognize_path(path)
|
||||||
return path, unused if unused.empty? # Found a perfect route -- we're finished.
|
raise RoutingError, "No route matches path #{path.inspect}" unless hash
|
||||||
if selected.nil? || unused.length < selected.last.length
|
|
||||||
failures << [selected.first, "A better url than #{selected[1]} was found."] if selected
|
controller = hash['controller']
|
||||||
selected = [route, path, unused]
|
hash['controller'] = controller.controller_path
|
||||||
|
request.path_parameters = hash
|
||||||
|
controller.new
|
||||||
|
end
|
||||||
|
alias :recognize! :recognize
|
||||||
|
|
||||||
|
def write_recognition
|
||||||
|
g = generator = CodeGeneration::RecognitionGenerator.new
|
||||||
|
g.finish_statement = Proc.new {|hash_expr| "return #{hash_expr}"}
|
||||||
|
|
||||||
|
g.def "self.recognize_path(path)" do
|
||||||
|
each do |route|
|
||||||
|
g << 'index = 0'
|
||||||
|
route.write_recognition(g)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
eval g.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def generation_code_for(ivar = 'routes', method_name = nil)
|
||||||
|
routes = instance_variable_get('@' + ivar)
|
||||||
|
key_ivar = "@keys_for_#{ivar}"
|
||||||
|
instance_variable_set(key_ivar, routes.collect {|route| route.keys})
|
||||||
|
|
||||||
|
g = generator = CodeGeneration::GenerationGenerator.new
|
||||||
|
g.def "self.#{method_name}(merged, options, expire_on)" do
|
||||||
|
g << 'unused_count = options.length + 1'
|
||||||
|
g << "unused_keys = keys = options.keys"
|
||||||
|
g << 'path = nil'
|
||||||
|
|
||||||
|
routes.each_with_index do |route, index|
|
||||||
|
g << "new_unused_keys = keys - #{key_ivar}[#{index}]"
|
||||||
|
g << 'new_path = ('
|
||||||
|
g.source.indent do
|
||||||
|
if index.zero?
|
||||||
|
g << "new_unused_count = new_unused_keys.length"
|
||||||
|
g << "hash = merged; not_expired = true"
|
||||||
|
route.write_generation(g.dup)
|
||||||
|
else
|
||||||
|
g.if "(new_unused_count = new_unused_keys.length) < unused_count" do |gp|
|
||||||
|
gp << "hash = merged; not_expired = true"
|
||||||
|
route.write_generation(gp)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
g.source.lines.last << ' )' # Add the closing brace to the end line
|
||||||
|
g.if 'new_path' do
|
||||||
|
g << 'return new_path, [] if new_unused_count.zero?'
|
||||||
|
g << 'path = new_path; unused_keys = new_unused_keys; unused_count = new_unused_count'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
g << "raise RoutingError, \"No url can be generated for the hash \#{options.inspect}\" unless path"
|
||||||
|
g << "return path, unused_keys"
|
||||||
|
end
|
||||||
|
|
||||||
|
return g
|
||||||
|
end
|
||||||
|
|
||||||
|
def categorize_routes
|
||||||
|
@categorized_routes = by_controller = Hash.new(self)
|
||||||
|
|
||||||
|
known_controllers.each do |name|
|
||||||
|
set = by_controller[name] = []
|
||||||
|
each do |route|
|
||||||
|
set << route if route.matches_controller? name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@categorized_routes
|
||||||
|
end
|
||||||
|
|
||||||
|
def known_controllers
|
||||||
|
@routes.inject([]) do |known, route|
|
||||||
|
if (controller = route.known[:controller])
|
||||||
|
if controller.is_a?(Regexp)
|
||||||
|
known << controller.source.scan(%r{[\w\d/]+}).select {|word| controller =~ word}
|
||||||
|
else known << controller
|
||||||
|
end
|
||||||
|
end
|
||||||
|
known
|
||||||
|
end.uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
def reload
|
||||||
|
NamedRoutes.clear
|
||||||
|
load(File.join(RAILS_ROOT, 'config', 'routes.rb'))
|
||||||
|
NamedRoutes.install
|
||||||
|
end
|
||||||
|
|
||||||
|
def connect(*args)
|
||||||
|
new_route = Route.new(*args)
|
||||||
|
@routes << new_route
|
||||||
|
return new_route
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw
|
||||||
|
old_routes = @routes
|
||||||
|
@routes = []
|
||||||
|
|
||||||
|
begin yield self
|
||||||
|
rescue
|
||||||
|
@routes = old_routes
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
write_generation
|
||||||
|
write_recognition
|
||||||
|
end
|
||||||
|
|
||||||
|
def empty?() @routes.empty? end
|
||||||
|
|
||||||
|
def each(&block) @routes.each(&block) end
|
||||||
|
|
||||||
|
def method_missing(name, *args)
|
||||||
|
return super(name, *args) unless args.length == 2
|
||||||
|
|
||||||
|
route = connect(*args)
|
||||||
|
NamedRoutes.name_route(route, name)
|
||||||
|
route
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module NamedRoutes
|
||||||
|
Helpers = []
|
||||||
|
class << self
|
||||||
|
def clear() Helpers.clear end
|
||||||
|
|
||||||
|
def hash_access_name(name)
|
||||||
|
"hash_for_#{name}_url"
|
||||||
|
end
|
||||||
|
|
||||||
|
def url_helper_name(name)
|
||||||
|
"#{name}_url"
|
||||||
|
end
|
||||||
|
|
||||||
|
def name_route(route, name)
|
||||||
|
hash = route.known.symbolize_keys
|
||||||
|
|
||||||
|
define_method(hash_access_name(name)) { hash }
|
||||||
|
module_eval(%{def #{url_helper_name name}(options = {})
|
||||||
|
url_for(#{hash_access_name(name)}.merge(options))
|
||||||
|
end})
|
||||||
|
|
||||||
|
protected url_helper_name(name), hash_access_name(name)
|
||||||
|
|
||||||
|
Helpers << url_helper_name(name)
|
||||||
|
Helpers.uniq!
|
||||||
|
end
|
||||||
|
|
||||||
|
def install(cls = ActionController::Base)
|
||||||
|
cls.send :include, self
|
||||||
|
if cls.respond_to? :helper_method
|
||||||
|
Helpers.each do |helper_name|
|
||||||
|
cls.send :helper_method, helper_name
|
||||||
end
|
end
|
||||||
end
|
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
|
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
|
|
||||||
request.path_parameters = options
|
|
||||||
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
|
|
||||||
route_file = defined?(RAILS_ROOT) ? File.join(RAILS_ROOT, 'config', 'routes') : nil
|
|
||||||
require_dependency(route_file) if route_file
|
|
||||||
rescue LoadError, ScriptError => e
|
|
||||||
raise RoutingError.new("Cannot load config/routes.rb:\n #{e.message}").copy_blame!(e)
|
|
||||||
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.extract_parameter_value(parameter) #:nodoc:
|
|
||||||
value = (parameter.respond_to?(:to_param) ? parameter.to_param : parameter).to_s
|
|
||||||
CGI.escape(value)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.draw(*args, &block) #:nodoc:
|
|
||||||
Routes.draw(*args) {|*args| block.call(*args)}
|
|
||||||
end
|
|
||||||
|
|
||||||
Routes = RouteSet.new
|
Routes = RouteSet.new
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,7 +12,7 @@ module ActionController
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_str
|
def to_str
|
||||||
"#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@parameters[:controller]}, #{@parameters[:action]}, #{@request.parameters.inspect}"
|
"#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@parameters[:controller]}, #{@parameters[:action]}, #{@request.parameters.inspect}"
|
||||||
end
|
end
|
||||||
|
|
||||||
alias_method :to_s, :to_str
|
alias_method :to_s, :to_str
|
||||||
|
@ -28,7 +28,7 @@ module ActionController
|
||||||
rewritten_url << '/' if options[:trailing_slash]
|
rewritten_url << '/' if options[:trailing_slash]
|
||||||
rewritten_url << "##{options[:anchor]}" if options[:anchor]
|
rewritten_url << "##{options[:anchor]}" if options[:anchor]
|
||||||
|
|
||||||
return rewritten_url
|
rewritten_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def rewrite_path(options)
|
def rewrite_path(options)
|
||||||
|
@ -38,17 +38,16 @@ module ActionController
|
||||||
path, extras = Routing::Routes.generate(options, @request)
|
path, extras = Routing::Routes.generate(options, @request)
|
||||||
|
|
||||||
if extras[:overwrite_params]
|
if extras[:overwrite_params]
|
||||||
params_copy = @request.parameters.reject { |k,v| ["controller","action"].include? k }
|
params_copy = @request.parameters.reject { |k,v| %w(controller action).include? k }
|
||||||
params_copy.update extras[:overwrite_params]
|
params_copy.update extras[:overwrite_params]
|
||||||
extras.delete(:overwrite_params)
|
extras.delete(:overwrite_params)
|
||||||
extras.update(params_copy)
|
extras.update(params_copy)
|
||||||
end
|
end
|
||||||
|
|
||||||
path = "/#{path.join('/')}".chomp '/'
|
path = "/#{path}"
|
||||||
path = '/' if path.empty?
|
path << build_query_string(extras) unless extras.empty?
|
||||||
path += build_query_string(extras)
|
|
||||||
|
|
||||||
return path
|
path
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a query string with escaped keys and values from the passed hash. If the passed hash contains an "id" it'll
|
# Returns a query string with escaped keys and values from the passed hash. If the passed hash contains an "id" it'll
|
||||||
|
@ -58,15 +57,14 @@ module ActionController
|
||||||
query_string = ""
|
query_string = ""
|
||||||
|
|
||||||
hash.each do |key, value|
|
hash.each do |key, value|
|
||||||
key = key.to_s
|
key = CGI.escape key.to_s
|
||||||
key = CGI.escape key
|
key << '[]' if value.class == Array
|
||||||
key += '[]' if value.class == Array
|
|
||||||
value = [ value ] unless value.class == Array
|
value = [ value ] unless value.class == Array
|
||||||
value.each { |val| elements << "#{key}=#{Routing.extract_parameter_value(val)}" }
|
value.each { |val| elements << "#{key}=#{Routing.extract_parameter_value(val)}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
query_string << ("?" + elements.join("&")) unless elements.empty?
|
query_string << ("?" + elements.join("&")) unless elements.empty?
|
||||||
return query_string
|
query_string
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,4 +9,4 @@ require 'action_controller/test_process'
|
||||||
|
|
||||||
ActionController::Base.logger = nil
|
ActionController::Base.logger = nil
|
||||||
ActionController::Base.ignore_missing_templates = true
|
ActionController::Base.ignore_missing_templates = true
|
||||||
ActionController::Routing::Routes.reload
|
ActionController::Routing::Routes.reload rescue nil
|
||||||
|
|
File diff suppressed because it is too large
Load diff
5
activesupport/lib/active_support/core_ext/array.rb
Normal file
5
activesupport/lib/active_support/core_ext/array.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
require File.dirname(__FILE__) + '/array/to_param'
|
||||||
|
|
||||||
|
class Array #:nodoc:
|
||||||
|
include ActiveSupport::CoreExtensions::Array::ToParam
|
||||||
|
end
|
12
activesupport/lib/active_support/core_ext/array/to_param.rb
Normal file
12
activesupport/lib/active_support/core_ext/array/to_param.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
module ActiveSupport #:nodoc:
|
||||||
|
module CoreExtensions #:nodoc:
|
||||||
|
module Array #:nodoc:
|
||||||
|
module ToParam #:nodoc:
|
||||||
|
# When an array is given to url_for, it is converted to a slash separated string.
|
||||||
|
def to_param
|
||||||
|
join '/'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
5
activesupport/lib/active_support/core_ext/cgi.rb
Normal file
5
activesupport/lib/active_support/core_ext/cgi.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
require File.dirname(__FILE__) + '/cgi/escape_skipping_slashes'
|
||||||
|
|
||||||
|
class CGI
|
||||||
|
extend(ActiveSupport::CoreExtensions::CGI::EscapeSkippingSlashes)
|
||||||
|
end
|
|
@ -0,0 +1,14 @@
|
||||||
|
module ActiveSupport #:nodoc:
|
||||||
|
module CoreExtensions #:nodoc:
|
||||||
|
module CGI #:nodoc:
|
||||||
|
module EscapeSkippingSlashes #:nodoc:
|
||||||
|
def escape_skipping_slashes(str)
|
||||||
|
str = str.join('/') if str.respond_to? :join
|
||||||
|
str.gsub(/([^ \/a-zA-Z0-9_.-])/n) do
|
||||||
|
"%#{$1.unpack('H2').first.upcase}"
|
||||||
|
end.tr(' ', '+')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -14,7 +14,7 @@ module Dependencies
|
||||||
end
|
end
|
||||||
|
|
||||||
def depend_on(file_name, swallow_load_errors = false)
|
def depend_on(file_name, swallow_load_errors = false)
|
||||||
if !loaded.include?(file_name)
|
unless loaded.include?(file_name)
|
||||||
loaded << file_name
|
loaded << file_name
|
||||||
|
|
||||||
begin
|
begin
|
||||||
|
@ -34,7 +34,7 @@ module Dependencies
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_or_load(file_name)
|
def require_or_load(file_name)
|
||||||
file_name = "#{file_name}.rb" unless ! load? || /\.rb$/ =~ file_name
|
file_name = "#{file_name}.rb" unless ! load? || file_name[-3..-1] == '.rb'
|
||||||
load? ? load(file_name) : require(file_name)
|
load? ? load(file_name) : require(file_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -52,8 +52,10 @@ module Dependencies
|
||||||
attr_reader :path
|
attr_reader :path
|
||||||
attr_reader :root
|
attr_reader :root
|
||||||
|
|
||||||
def self.root(*load_paths)
|
class << self
|
||||||
RootLoadingModule.new(*load_paths)
|
def root(*load_paths)
|
||||||
|
RootLoadingModule.new(*load_paths)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(root, path=[])
|
def initialize(root, path=[])
|
||||||
|
@ -61,7 +63,7 @@ module Dependencies
|
||||||
@root = root
|
@root = root
|
||||||
end
|
end
|
||||||
|
|
||||||
def root?() self.root == self end
|
def root?() self.root == self end
|
||||||
def load_paths() self.root.load_paths end
|
def load_paths() self.root.load_paths end
|
||||||
|
|
||||||
# Load missing constants if possible.
|
# Load missing constants if possible.
|
||||||
|
@ -71,13 +73,15 @@ module Dependencies
|
||||||
|
|
||||||
# Load the controller class or a parent module.
|
# Load the controller class or a parent module.
|
||||||
def const_load!(name, file_name = nil)
|
def const_load!(name, file_name = nil)
|
||||||
|
file_name ||= 'application' if root? && name.to_s == 'ApplicationController'
|
||||||
path = self.path + [file_name || name]
|
path = self.path + [file_name || name]
|
||||||
|
|
||||||
load_paths.each do |load_path|
|
load_paths.each do |load_path|
|
||||||
fs_path = load_path.filesystem_path(path)
|
fs_path = load_path.filesystem_path(path)
|
||||||
next unless fs_path
|
next unless fs_path
|
||||||
|
|
||||||
if File.directory?(fs_path)
|
case
|
||||||
|
when File.directory?(fs_path)
|
||||||
new_module = LoadingModule.new(self.root, self.path + [name])
|
new_module = LoadingModule.new(self.root, self.path + [name])
|
||||||
self.const_set name, new_module
|
self.const_set name, new_module
|
||||||
if self.root?
|
if self.root?
|
||||||
|
@ -88,7 +92,7 @@ module Dependencies
|
||||||
Object.const_set(name, new_module)
|
Object.const_set(name, new_module)
|
||||||
end
|
end
|
||||||
break
|
break
|
||||||
elsif File.file?(fs_path)
|
when File.file?(fs_path)
|
||||||
self.root.load_file!(fs_path)
|
self.root.load_file!(fs_path)
|
||||||
|
|
||||||
# Import the loaded constant from Object provided we are the root node.
|
# Import the loaded constant from Object provided we are the root node.
|
||||||
|
@ -97,7 +101,7 @@ module Dependencies
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return self.const_defined?(name)
|
self.const_defined?(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Is this name present or loadable?
|
# Is this name present or loadable?
|
||||||
|
@ -141,7 +145,7 @@ module Dependencies
|
||||||
# if the path leads to a module, or the path to a file if it leads to an object.
|
# if the path leads to a module, or the path to a file if it leads to an object.
|
||||||
def filesystem_path(path, allow_module=true)
|
def filesystem_path(path, allow_module=true)
|
||||||
fs_path = [@root]
|
fs_path = [@root]
|
||||||
fs_path += path[0..-2].collect {|name| const_name_to_module_name name}
|
fs_path += path[0..-2].map {|name| const_name_to_module_name name}
|
||||||
|
|
||||||
if allow_module
|
if allow_module
|
||||||
result = File.join(fs_path, const_name_to_module_name(path.last))
|
result = File.join(fs_path, const_name_to_module_name(path.last))
|
||||||
|
@ -150,7 +154,7 @@ module Dependencies
|
||||||
|
|
||||||
result = File.join(fs_path, const_name_to_file_name(path.last))
|
result = File.join(fs_path, const_name_to_file_name(path.last))
|
||||||
|
|
||||||
return File.file?(result) ? result : nil
|
File.file?(result) ? result : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def const_name_to_file_name(name)
|
def const_name_to_file_name(name)
|
||||||
|
@ -164,8 +168,8 @@ module Dependencies
|
||||||
end
|
end
|
||||||
|
|
||||||
Object.send(:define_method, :require_or_load) { |file_name| Dependencies.require_or_load(file_name) } unless Object.respond_to?(:require_or_load)
|
Object.send(:define_method, :require_or_load) { |file_name| Dependencies.require_or_load(file_name) } unless Object.respond_to?(:require_or_load)
|
||||||
Object.send(:define_method, :require_dependency) { |file_name| Dependencies.depend_on(file_name) } unless Object.respond_to?(:require_dependency)
|
Object.send(:define_method, :require_dependency) { |file_name| Dependencies.depend_on(file_name) } unless Object.respond_to?(:require_dependency)
|
||||||
Object.send(:define_method, :require_association) { |file_name| Dependencies.associate_with(file_name) } unless Object.respond_to?(:require_association)
|
Object.send(:define_method, :require_association) { |file_name| Dependencies.associate_with(file_name) } unless Object.respond_to?(:require_association)
|
||||||
|
|
||||||
class Module #:nodoc:
|
class Module #:nodoc:
|
||||||
# Use const_missing to autoload associations so we don't have to
|
# Use const_missing to autoload associations so we don't have to
|
||||||
|
@ -186,19 +190,17 @@ end
|
||||||
|
|
||||||
class Object #:nodoc:
|
class Object #:nodoc:
|
||||||
def load(file, *extras)
|
def load(file, *extras)
|
||||||
begin super(file, *extras)
|
super(file, *extras)
|
||||||
rescue Object => exception
|
rescue Object => exception
|
||||||
exception.blame_file! file
|
exception.blame_file! file
|
||||||
raise
|
raise
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def require(file, *extras)
|
def require(file, *extras)
|
||||||
begin super(file, *extras)
|
super(file, *extras)
|
||||||
rescue Object => exception
|
rescue Object => exception
|
||||||
exception.blame_file! file
|
exception.blame_file! file
|
||||||
raise
|
raise
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
14
activesupport/test/core_ext/array_ext_test.rb
Normal file
14
activesupport/test/core_ext/array_ext_test.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
require 'test/unit'
|
||||||
|
require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/array'
|
||||||
|
|
||||||
|
class ArrayExtToParamTests < Test::Unit::TestCase
|
||||||
|
def test_string_array
|
||||||
|
assert_equal '', %w().to_param
|
||||||
|
assert_equal 'hello/world', %w(hello world).to_param
|
||||||
|
assert_equal 'hello/10', %w(hello 10).to_param
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_number_array
|
||||||
|
assert_equal '10/20', [10, 20].to_param
|
||||||
|
end
|
||||||
|
end
|
15
activesupport/test/core_ext/cgi_ext_test.rb
Normal file
15
activesupport/test/core_ext/cgi_ext_test.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
require 'test/unit'
|
||||||
|
require File.dirname(__FILE__) + '/../../lib/active_support/core_ext/cgi'
|
||||||
|
|
||||||
|
class EscapeSkippingSlashesTest < Test::Unit::TestCase
|
||||||
|
def test_array
|
||||||
|
assert_equal 'hello/world', CGI.escape_skipping_slashes(%w(hello world))
|
||||||
|
assert_equal 'hello+world/how/are/you', CGI.escape_skipping_slashes(['hello world', 'how', 'are', 'you'])
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_typical
|
||||||
|
assert_equal 'hi', CGI.escape_skipping_slashes('hi')
|
||||||
|
assert_equal 'hi/world', CGI.escape_skipping_slashes('hi/world')
|
||||||
|
assert_equal 'hi/world+you+funky+thing', CGI.escape_skipping_slashes('hi/world you funky thing')
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue