aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2005-06-24 16:40:01 +0000
committerDavid Heinemeier Hansson <david@loudthinking.com>2005-06-24 16:40:01 +0000
commit8e56f5ea3e5394caa2ffee466a7395876c288c2a (patch)
tree6200c1ae2b73181cd214ab060615283f93eb9094 /actionpack
parent540d005ca5d7c4f462a041751dba438af0e281a2 (diff)
downloadrails-8e56f5ea3e5394caa2ffee466a7395876c288c2a.tar.gz
rails-8e56f5ea3e5394caa2ffee466a7395876c288c2a.tar.bz2
rails-8e56f5ea3e5394caa2ffee466a7395876c288c2a.zip
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
Diffstat (limited to 'actionpack')
-rw-r--r--actionpack/CHANGELOG8
-rwxr-xr-xactionpack/lib/action_controller/base.rb3
-rw-r--r--actionpack/lib/action_controller/code_generation.rb235
-rwxr-xr-xactionpack/lib/action_controller/request.rb13
-rw-r--r--actionpack/lib/action_controller/routing.rb780
-rw-r--r--actionpack/lib/action_controller/url_rewriter.rb20
-rw-r--r--actionpack/test/abstract_unit.rb2
-rw-r--r--actionpack/test/controller/routing_test.rb1020
8 files changed, 1331 insertions, 750 deletions
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG
index 1a40f75942..ea569c1c7c 100644
--- a/actionpack/CHANGELOG
+++ b/actionpack/CHANGELOG
@@ -1,8 +1,12 @@
*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]
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index 243693437f..bd0e78719d 100755
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -1,6 +1,7 @@
require 'action_controller/request'
require 'action_controller/response'
require 'action_controller/routing'
+require 'action_controller/code_generation'
require 'action_controller/url_rewriter'
require 'drb'
@@ -11,7 +12,7 @@ module ActionController #:nodoc:
end
class MissingTemplate < ActionControllerError #:nodoc:
end
- class RoutingError < ActionControllerError#:nodoc:
+ class RoutingError < ActionControllerError #:nodoc:
attr_reader :failures
def initialize(message, failures=[])
super(message)
diff --git a/actionpack/lib/action_controller/code_generation.rb b/actionpack/lib/action_controller/code_generation.rb
new file mode 100644
index 0000000000..6519980198
--- /dev/null
+++ b/actionpack/lib/action_controller/code_generation.rb
@@ -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
diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb
index 8994f42915..7a8fe49d0f 100755
--- a/actionpack/lib/action_controller/request.rb
+++ b/actionpack/lib/action_controller/request.rb
@@ -38,7 +38,6 @@ module ActionController
method == :head
end
-
# Determine whether the body of a POST request is URL-encoded (default),
# XML, or YAML by checking the Content-Type HTTP header:
#
@@ -78,9 +77,9 @@ module ActionController
post_format == :yaml && post?
end
- # Is the X-Requested-With HTTP header present and does it contain the
- # string "XMLHttpRequest"?. The Prototype Javascript library sends this
- # header with every Ajax request.
+ # Returns true if the request's "X-Requested-With" header contains
+ # "XMLHttpRequest". (The Prototype Javascript library sends this header with
+ # every Ajax request.)
def xml_http_request?
not /XMLHttpRequest/i.match(env['HTTP_X_REQUESTED_WITH']).nil?
end
@@ -186,7 +185,11 @@ module ActionController
def 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
def path_parameters
diff --git a/actionpack/lib/action_controller/routing.rb b/actionpack/lib/action_controller/routing.rb
index f0003e9974..12464f9dcf 100644
--- a/actionpack/lib/action_controller/routing.rb
+++ b/actionpack/lib/action_controller/routing.rb
@@ -1,342 +1,578 @@
module ActionController
- # See http://manuals.rubyonrails.com/read/chapter/65
module Routing
- class Route #:nodoc:
- attr_reader :defaults # The defaults hash
-
- def initialize(path, hash={})
- raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash)
- @defaults = hash[:defaults].kind_of?(Hash) ? hash.delete(:defaults) : {}
- @requirements = hash[:requirements].kind_of?(Hash) ? hash.delete(:requirements) : {}
- self.items = path
+ class << self
+
+ def expiry_hash(options, recall)
+ k = v = nil
+ expire_on = {}
+ options.each {|k, v| expire_on[k] = ((rcv = recall[k]) && (rcv != v))}
+ expire_on
+ 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|
- raise TypeError, "Hash keys must be symbols!" unless k.kind_of? Symbol
- if v.kind_of? Regexp
- raise ArgumentError, "Regexp requirement on #{k}, but #{k} is not in this route's path!" unless @items.include? k
- @requirements[k] = v
+ hash[k] = (v.respond_to? :to_param) ? v.to_param.to_s : v.to_s
+ end
+ hash
+ 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
- (@items.include?(k) ? @defaults : @requirements)[k] = (v.nil? ? nil : v.to_s)
- end
+ raise ArgumentError, "Valid criteria are strings, regular expressions, true, or nil"
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
+ end
+
+ class Component #:nodoc
+ def dynamic?() false end
+ def optional?() false end
+
+ def key() nil end
+
+ def self.new(string, *args)
+ 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
- @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
+
+ class StaticComponent < Component #:nodoc
+ attr_reader :value
+
+ def initialize(value)
+ @value = value
+ end
+
+ def write_recognition(g)
+ g.if_next_matches(value) do |gp|
+ gp.move_forward {|gpp| gpp.continue}
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
- collection = false
+ def write_generation(g)
+ g.add_segment(value) {|gp| gp.continue }
+ end
+ end
- if /^\*/ =~ item.to_s
- collection = true
- item = item.to_s.sub(/^\*/,"").intern
- end
+ class DynamicComponent < Component #:nodoc
+ attr_reader :key, :default
+ attr_accessor :condition
+
+ def dynamic?() true end
+ def optional?() @optional end
- used_names[item] = true
- value = options[item] || defaults[item] || @defaults[item]
- return nil, requirements_for(item) unless passes_requirements?(item, value)
+ def default=(default)
+ @optional = true
+ @default = default
+ end
+
+ def initialize(key, options = {})
+ @key = key.to_sym
+ @default, @condition = options[:default], options[:condition]
+ @optional = options.key?(:default)
+ end
- defaults = {} unless defaults == {} || value == defaults[item] # Stop using defaults if this component isn't the same as the default.
+ 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
- if value.nil? || item == :controller
- value
- elsif collection
- if value.kind_of?(Array)
- value = value.collect {|v| Routing.extract_parameter_value(v)}.join('/')
- else
- value = Routing.extract_parameter_value(value).gsub(/%2F/, "/")
- end
- value
- else
- Routing.extract_parameter_value(value)
- end
- else
- item
- 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
-
- @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
+
+ 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 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
+
+ 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
-
- components.collect! {|c| c.to_s}
- return components, unused
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.
- # 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
+ while index < length
+ return nil unless /^[a-z][a-z\d_]*$/ =~ (segment = segments[index])
+ index += 1
- @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
- 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
+ mod_name = segment.camelize
+ controller_name = "#{mod_name}Controller"
+
+ return eval("mod::#{controller_name}"), (index - start_at) if mod.const_available?(controller_name)
+ return nil unless mod.const_available?(mod_name)
+ mod = eval("mod::#{mod_name}")
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
+ 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
- @requirements.each {|k,v| options[k] ||= v unless v.kind_of?(Regexp)}
+ 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
- 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
+ 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
- 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}>"
+ "<#{self.class} #{path.inspect}, #{options.inspect[1..-1]}>"
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
- 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]
+
+ 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
- return nil, nil # Path ended, but no controller found.
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
+
+ def initialize_components(path)
+ path = path.split('/') if path.is_a? String
+ self.components = path.collect {|str| Component.new str}
+ 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)
- items = path.split('/').collect {|c| (/^(:|\*)(\w+)$/ =~ c) ? (($1 == ':' ) ? $2.intern : "*#{$2}".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
+ options.each do |k, v|
+ if path_keys.include?(k) then (v.is_a?(Regexp) ? conditions : defaults)[k] = v
+ else known[k] = v
end
- seen
end
+ [defaults, conditions]
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
- 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"
+ component.condition = conditions[component.key] if conditions.key?(component.key)
end
end
end
-
- class RouteSet#:nodoc:
+
+ class RouteSet
+ attr_reader :routes, :categories, :controller_to_selector
def initialize
@routes = []
+ @generation_methods = Hash.new(:generate_default_path)
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?
+ def generate(options, request_or_recall_hash = {})
+ recall = request_or_recall_hash.is_a?(Hash) ? request_or_recall_hash : request_or_recall_hash.symbolized_path_parameters
+
+ if ((rc_c = recall[:controller]) && rc_c.include?(?/)) || ((c = options[:controller]) && c.include?(?/))
+ options[:controller] = Routing.controller_relative_to(c, rc_c)
+ end
+ 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 each
- @routes.each {|route| yield route}
+
+ 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)
- # 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.
+ @generation_methods[controller.to_s] = method_name
+ @generation_methods[controller.to_sym] = method_name
end
- defaults.delete_if {|k, v| options.key?(k) && options[k].nil?} # Remove defaults that have been manually cleared using :name => nil
-
- 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
+
+ eval(generation_code_for('routes', 'generate_default_path').to_s)
+ end
+
+ def recognize(request)
+ string_path = request.path
+ string_path.chomp! if string_path[0] == ?/
+ path = string_path.split '/'
+ path.shift
+
+ hash = recognize_path(path)
+ raise RoutingError, "No route matches path #{path.inspect}" unless hash
+
+ controller = hash['controller']
+ 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
-
- return selected[1..-1] unless selected.nil?
- raise RoutingError.new("Generation failure: No route for url_options #{options.inspect}, defaults: #{defaults.inspect}", failures)
+
+ eval g.to_s
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
+ 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
- raise RoutingError.new("No route for path: #{path.join('/').inspect}", failures)
+ return g
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)
+ 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
- else
- options[:controller] = defaults[:controller]
end
+
+ @categorized_routes
end
- def route(*args)
- add_route(Route.new(*args))
+ 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
- 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?
+ 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 draw
- @routes.clear
- yield self
+ 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 self.extract_parameter_value(parameter) #:nodoc:
- value = (parameter.respond_to?(:to_param) ? parameter.to_param : parameter).to_s
- CGI.escape(value)
+ 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
- def self.draw(*args, &block) #:nodoc:
- Routes.draw(*args) {|*args| block.call(*args)}
- end
-
Routes = RouteSet.new
end
end
diff --git a/actionpack/lib/action_controller/url_rewriter.rb b/actionpack/lib/action_controller/url_rewriter.rb
index 4313340892..c9fd60880c 100644
--- a/actionpack/lib/action_controller/url_rewriter.rb
+++ b/actionpack/lib/action_controller/url_rewriter.rb
@@ -12,7 +12,7 @@ module ActionController
end
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
alias_method :to_s, :to_str
@@ -28,7 +28,7 @@ module ActionController
rewritten_url << '/' if options[:trailing_slash]
rewritten_url << "##{options[:anchor]}" if options[:anchor]
- return rewritten_url
+ rewritten_url
end
def rewrite_path(options)
@@ -38,17 +38,16 @@ module ActionController
path, extras = Routing::Routes.generate(options, @request)
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]
extras.delete(:overwrite_params)
extras.update(params_copy)
end
- path = "/#{path.join('/')}".chomp '/'
- path = '/' if path.empty?
- path += build_query_string(extras)
+ path = "/#{path}"
+ path << build_query_string(extras) unless extras.empty?
- return path
+ path
end
# 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 = ""
hash.each do |key, value|
- key = key.to_s
- key = CGI.escape key
- key += '[]' if value.class == Array
+ key = CGI.escape key.to_s
+ key << '[]' if value.class == Array
value = [ value ] unless value.class == Array
value.each { |val| elements << "#{key}=#{Routing.extract_parameter_value(val)}" }
end
query_string << ("?" + elements.join("&")) unless elements.empty?
- return query_string
+ query_string
end
end
end
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index 4b244e9472..fae40cd8ac 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -9,4 +9,4 @@ require 'action_controller/test_process'
ActionController::Base.logger = nil
ActionController::Base.ignore_missing_templates = true
-ActionController::Routing::Routes.reload \ No newline at end of file
+ActionController::Routing::Routes.reload rescue nil
diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb
index 0f5987e69e..2e35320322 100644
--- a/actionpack/test/controller/routing_test.rb
+++ b/actionpack/test/controller/routing_test.rb
@@ -1,543 +1,647 @@
-# Code Generated by ZenTest v. 2.3.0
-# Couldn't find class for name Routing
-# classname: asrt / meth = ratio%
-# ActionController::Routing::RouteSet: 0 / 16 = 0.00%
-# ActionController::Routing::RailsRoute: 0 / 4 = 0.00%
-# ActionController::Routing::Route: 0 / 8 = 0.00%
-
require File.dirname(__FILE__) + '/../abstract_unit'
require 'test/unit'
-require 'cgi'
-class FakeController
- attr_reader :controller_path
- attr_reader :name
- def initialize(name, controller_path)
- @name = name
- @controller_path = controller_path
- end
- def kind_of?(x)
- x === Class || x == FakeController
- end
-end
+RunTimeTests = ARGV.include? 'time'
-module Controllers
- module Admin
- UserController = FakeController.new 'Admin::UserController', 'admin/user'
- AccessController = FakeController.new 'Admin::AccessController', 'admin/access'
+module ActionController::CodeGeneration
+
+class SourceTests < Test::Unit::TestCase
+ attr_accessor :source
+ def setup
+ @source = Source.new
end
- module Editing
- PageController = FakeController.new 'Editing::PageController', 'editing/page'
- ImageController = FakeController.new 'Editing::ImageController', 'editing/image'
+
+ def test_initial_state
+ assert_equal [], source.lines
+ assert_equal 0, source.indentation_level
end
- module User
- NewsController = FakeController.new 'User::NewsController', 'user/news'
- PaymentController = FakeController.new 'User::PaymentController', 'user/payment'
+
+ def test_trivial_operations
+ source << "puts 'Hello World'"
+ assert_equal ["puts 'Hello World'"], source.lines
+ assert_equal "puts 'Hello World'", source.to_s
+
+ source.line "puts 'Goodbye World'"
+ assert_equal ["puts 'Hello World'", "puts 'Goodbye World'"], source.lines
+ assert_equal "puts 'Hello World'\nputs 'Goodbye World'", source.to_s
end
- ContentController = FakeController.new 'ContentController', 'content'
- ResourceController = FakeController.new 'ResourceController', 'resource'
-end
-# Extend the modules with the required methods...
-[Controllers, Controllers::Admin, Controllers::Editing, Controllers::User].each do |mod|
- mod.instance_eval('alias :const_available? :const_defined?')
- mod.constants.each {|k| Object.const_set(k, mod.const_get(k))} # export the modules & controller classes.
-end
-
+ def test_indentation
+ source << "x = gets.to_i"
+ source << 'if x.odd?'
+ source.indent { source << "puts 'x is odd!'" }
+ source << 'else'
+ source.indent { source << "puts 'x is even!'" }
+ source << 'end'
+
+ assert_equal ["x = gets.to_i", "if x.odd?", " puts 'x is odd!'", 'else', " puts 'x is even!'", 'end'], source.lines
+
+ text = "x = gets.to_i
+if x.odd?
+ puts 'x is odd!'
+else
+ puts 'x is even!'
+end"
-class RouteTests < Test::Unit::TestCase
- def route(*args)
- return @route if @route && (args.empty? || @args == args)
- @args = args
- @route = ActionController::Routing::Route.new(*args)
- return @route
- end
+ assert_equal text, source.to_s
+ end
+end
+class CodeGeneratorTests < Test::Unit::TestCase
+ attr_accessor :generator
def setup
- self.route '/:controller/:action/:id'
- @defaults = {:controller => 'content', :action => 'show', :id => '314'}
+ @generator = CodeGenerator.new
end
-
- # Don't put a leading / on the url.
- # Make sure the controller is one from the above fake Controllers module.
- def verify_recognize(url, expected_options, reason='')
- url = url.split('/') if url.kind_of? String
- reason = ": #{reason}" unless reason.empty?
- controller_class, options = @route.recognize(url)
- assert_not_equal nil, controller_class, "#{@route.inspect} didn't recognize #{url}#{reason}\n #{options}"
- assert_equal expected_options, options, "#{@route.inspect} produced wrong options for #{url}#{reason}"
+
+ def test_initial_state
+ assert_equal [], generator.source.lines
+ assert_equal [], generator.locals
end
-
- # The expected url should not have a leading /
- # You can use @defaults if you want a set of plausible defaults
- def verify_generate(expected_url, expected_extras, options, defaults, reason='')
- reason = "#{reason}: " unless reason.empty?
- components, extras = @route.generate(options, defaults)
- assert_not_equal nil, components, "#{reason}#{@route.inspect} didn't generate for \n options = #{options.inspect}\n defaults = #{defaults.inspect}\n #{extras}"
- assert_equal expected_extras, extras, "#{reason} #{@route.inspect}.generate: incorrect extra's"
- assert_equal expected_url, components.join('/'), "#{reason} #{@route.inspect}.generate: incorrect url"
+
+ def test_trivial_operations
+ ["puts 'Hello World'", "puts 'Goodbye World'"].each {|l| generator << l}
+ assert_equal ["puts 'Hello World'", "puts 'Goodbye World'"], generator.source.lines
+ assert_equal "puts 'Hello World'\nputs 'Goodbye World'", generator.to_s
end
- def test_recognize_default_unnested_with_action_and_id
- verify_recognize('content/action/id', {:controller => 'content', :action => 'action', :id => 'id'})
- verify_recognize('content/show/10', {:controller => 'content', :action => 'show', :id => '10'})
- end
- def test_generate_default_unnested_with_action_and_id_no_extras
- verify_generate('content/action/id', {}, {:controller => 'content', :action => 'action', :id => 'id'}, @defaults)
- verify_generate('content/show/10', {}, {:controller => 'content', :action => 'show', :id => '10'}, @defaults)
- end
- def test_generate_default_unnested_with_action_and_id
- verify_generate('content/action/id', {:a => 'a'}, {:controller => 'content', :action => 'action', :id => 'id', :a => 'a'}, @defaults)
- verify_generate('content/show/10', {:a => 'a'}, {:controller => 'content', :action => 'show', :id => '10', :a => 'a'}, @defaults)
+ def test_if
+ generator << "x = gets.to_i"
+ generator.if("x.odd?") { generator << "puts 'x is odd!'" }
+
+ assert_equal "x = gets.to_i\nif x.odd?\n puts 'x is odd!'\nend", generator.to_s
end
- # Note that we can't put tests here for proper relative controller handline
- # because that is handled by RouteSet.
- def test_recognize_default_nested_with_action_and_id
- verify_recognize('admin/user/action/id', {:controller => 'admin/user', :action => 'action', :id => 'id'})
- verify_recognize('admin/user/show/10', {:controller => 'admin/user', :action => 'show', :id => '10'})
+ def test_else
+ test_if
+ generator.else { generator << "puts 'x is even!'" }
+
+ assert_equal "x = gets.to_i\nif x.odd?\n puts 'x is odd!'\nelse \n puts 'x is even!'\nend", generator.to_s
end
- def test_generate_default_nested_with_action_and_id_no_extras
- verify_generate('admin/user/action/id', {}, {:controller => 'admin/user', :action => 'action', :id => 'id'}, @defaults)
- verify_generate('admin/user/show/10', {}, {:controller => 'admin/user', :action => 'show', :id => '10'}, @defaults)
+
+ def test_dup
+ generator << 'x = 2'
+ generator.locals << :x
+
+ g = generator.dup
+ assert_equal generator.source, g.source
+ assert_equal generator.locals, g.locals
+
+ g << 'y = 3'
+ g.locals << :y
+ assert_equal [:x, :y], g.locals # Make sure they don't share the same array.
+ assert_equal [:x], generator.locals
end
- def test_generate_default_nested_with_action_and_id_relative_to_root
- verify_generate('admin/user/action/id', {:a => 'a'}, {:controller => 'admin/user', :action => 'action', :id => 'id', :a => 'a'}, @defaults)
- verify_generate('admin/user/show/10', {:a => 'a'}, {:controller => 'admin/user', :action => 'show', :id => '10', :a => 'a'}, @defaults)
+end
+
+# XXX Extract to test/controller/fake_controllers.rb
+module Object::Controllers
+ def self.const_available?(*args)
+ const_defined?(*args)
end
- def test_recognize_default_nested_with_action
- verify_recognize('admin/user/action', {:controller => 'admin/user', :action => 'action'})
- verify_recognize('admin/user/show', {:controller => 'admin/user', :action => 'show'})
- end
- def test_generate_default_nested_with_action_no_extras
- verify_generate('admin/user/action', {}, {:controller => 'admin/user', :action => 'action'}, @defaults)
- verify_generate('admin/user/show', {}, {:controller => 'admin/user', :action => 'show'}, @defaults)
+ class ContentController
end
- def test_generate_default_nested_with_action
- verify_generate('admin/user/action', {:a => 'a'}, {:controller => 'admin/user', :action => 'action', :a => 'a'}, @defaults)
- verify_generate('admin/user/show', {:a => 'a'}, {:controller => 'admin/user', :action => 'show', :a => 'a'}, @defaults)
+ module Admin
+ def self.const_available?(*args)
+ const_defined?(*args)
+ end
+
+ class UserController
+ end
end
+end
- def test_recognize_default_nested_with_id_and_index
- verify_recognize('admin/user/index/hello', {:controller => 'admin/user', :id => 'hello', :action => 'index'})
- verify_recognize('admin/user/index/10', {:controller => 'admin/user', :id => "10", :action => 'index'})
- end
- def test_generate_default_nested_with_id_no_extras
- verify_generate('admin/user/index/hello', {}, {:controller => 'admin/user', :id => 'hello'}, @defaults)
- verify_generate('admin/user/index/10', {}, {:controller => 'admin/user', :id => 10}, @defaults)
+
+class RecognitionTests < Test::Unit::TestCase
+ attr_accessor :generator
+ alias :g :generator
+ def setup
+ @generator = RecognitionGenerator.new
end
- def test_generate_default_nested_with_id
- verify_generate('admin/user/index/hello', {:a => 'a'}, {:controller => 'admin/user', :id => 'hello', :a => 'a'}, @defaults)
- verify_generate('admin/user/index/10', {:a => 'a'}, {:controller => 'admin/user', :id => 10, :a => 'a'}, @defaults)
+
+ def go(components)
+ g.current = components.first
+ g.after = components[1..-1] || []
+ g.go
end
- def test_recognize_default_nested
- verify_recognize('admin/user', {:controller => 'admin/user', :action => 'index'})
- verify_recognize('admin/user', {:controller => 'admin/user', :action => 'index'})
- end
- def test_generate_default_nested_no_extras
- verify_generate('admin/user', {}, {:controller => 'admin/user'}, @defaults)
- verify_generate('admin/user', {}, {:controller => 'admin/user'}, @defaults)
- end
- def test_generate_default_nested
- verify_generate('admin/user', {:a => 'a'}, {:controller => 'admin/user', :a => 'a'}, @defaults)
- verify_generate('admin/user', {:a => 'a'}, {:controller => 'admin/user', :a => 'a'}, @defaults)
+ def execute(path, show = false)
+ path = path.split('/') if path.is_a? String
+ source = "index, path = 0, #{path.inspect}\n#{g.to_s}"
+ puts source if show
+ r = eval source
+ r ? r.symbolize_keys : nil
end
- # Test generate with a default controller set.
- def test_generate_default_controller
- route '/:controller/:action/:id', :action => 'index', :id => nil, :controller => 'content'
- @defaults[:controller] = 'resource'
-
- verify_generate('', {}, {:controller => 'content'}, @defaults)
- verify_generate('', {}, {:controller => 'content', :action => 'index'}, @defaults)
- verify_generate('content/not-index', {}, {:controller => 'content', :action => 'not-index'}, @defaults)
- verify_generate('content/index/10', {}, {:controller => 'content', :id => 10}, @defaults)
- verify_generate('content/index/hi', {}, {:controller => 'content', :action => 'index', :id => 'hi'}, @defaults)
- verify_generate('', {:a => 'a'}, {:controller => 'content', :a => 'a'}, @defaults)
- verify_generate('', {:a => 'a'}, {:controller => 'content', :a => 'a'}, @defaults)
-
- # Call some other generator tests
- test_generate_default_unnested_with_action_and_id
- test_generate_default_nested_with_action_and_id_no_extras
- test_generate_default_nested_with_id
- test_generate_default_nested_with_id_no_extras
- end
-
- # Test generate with a default controller set.
- def test_generate_default_controller
- route '/:controller/:action/:id', :action => 'index', :id => nil, :controller => 'content'
- @defaults[:controller] = 'resource'
- verify_recognize('', {:controller => 'content', :action => 'index'})
- verify_recognize('content', {:controller => 'content', :action => 'index'})
- verify_recognize('content/index', {:controller => 'content', :action => 'index'})
- verify_recognize('content/index/10', {:controller => 'content', :action => 'index', :id => '10'})
- end
- # Make sure generation & recognition don't happen in some cases:
- def test_no_generate_on_no_options
- assert_equal nil, @route.generate({}, {})[0]
- end
- def test_requirements
- route 'some_static/route', :controller => 'content'
- assert_equal nil, @route.generate({}, {})[0]
- assert_equal nil, @route.generate({:controller => "dog"}, {})[0]
- assert_equal nil, @route.recognize([])[0]
- assert_equal nil, @route.recognize(%w{some_static route with more than expected})[0]
- end
+ Static = ::ActionController::Routing::StaticComponent
+ Dynamic = ::ActionController::Routing::DynamicComponent
+ Path = ::ActionController::Routing::PathComponent
+ Controller = ::ActionController::Routing::ControllerComponent
- def test_basecamp
- route 'clients/', :controller => 'content'
- verify_generate('clients', {}, {:controller => 'content'}, {}) # Would like to have clients/
- verify_generate('clients', {}, {:controller => 'content'}, @defaults)
+ def test_all_static
+ c = %w(hello world how are you).collect {|str| Static.new(str)}
+
+ g.result :controller, "::Controllers::ContentController", true
+ g.constant_result :action, 'index'
+
+ go c
+
+ assert_nil execute('x')
+ assert_nil execute('hello/world/how')
+ assert_nil execute('hello/world/how/are')
+ assert_nil execute('hello/world/how/are/you/today')
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'index'}, execute('hello/world/how/are/you'))
end
- def test_regexp_requirements
- const_options = {:controller => 'content', :action => 'by_date'}
- route ':year/:month/:day', const_options.merge(:year => /\d{4}/, :month => /\d{1,2}/, :day => /\d{1,2}/)
- verify_recognize('2004/01/02', const_options.merge(:year => '2004', :month => '01', :day => '02'))
- verify_recognize('2004/1/2', const_options.merge(:year => '2004', :month => '1', :day => '2'))
- assert_equal nil, @route.recognize(%w{200 10 10})[0]
- assert_equal nil, @route.recognize(%w{content show 10})[0]
+ def test_basic_dynamic
+ c = [Static.new("hi"), Dynamic.new(:action)]
+ g.result :controller, "::Controllers::ContentController", true
+ go c
- verify_generate('2004/01/02', {}, const_options.merge(:year => '2004', :month => '01', :day => '02'), @defaults)
- verify_generate('2004/1/2', {}, const_options.merge(:year => '2004', :month => '1', :day => '2'), @defaults)
- assert_equal nil, @route.generate(const_options.merge(:year => '12004', :month => '01', :day => '02'), @defaults)[0]
- end
-
- def test_regexp_requirement_not_in_path
- assert_raises(ArgumentError) {route 'constant/path', :controller => 'content', :action => 'by_date', :something => /\d+/}
- end
-
- def test_special_hash_names
- route ':year/:name', :requirements => {:year => /\d{4}/, :controller => 'content'}, :defaults => {:name => 'ulysses'}, :action => 'show_bio_year'
- verify_generate('1984', {}, {:controller => 'content', :action => 'show_bio_year', :year => 1984}, @defaults)
- verify_generate('1984', {}, {:controller => 'content', :action => 'show_bio_year', :year => '1984'}, @defaults)
- verify_generate('1984/odessys', {}, {:controller => 'content', :action => 'show_bio_year', :year => 1984, :name => 'odessys'}, @defaults)
- verify_generate('1984/odessys', {}, {:controller => 'content', :action => 'show_bio_year', :year => '1984', :name => 'odessys'}, @defaults)
-
- verify_recognize('1984/odessys', {:controller => 'content', :action => 'show_bio_year', :year => '1984', :name => 'odessys'})
- verify_recognize('1984', {:controller => 'content', :action => 'show_bio_year', :year => '1984', :name => 'ulysses'})
+ assert_nil execute('boo')
+ assert_nil execute('boo/blah')
+ assert_nil execute('hi')
+ assert_nil execute('hi/dude/what')
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'dude'}, execute('hi/dude'))
+ end
+
+ def test_dynamic_with_default
+ c = [Static.new("hi"), Dynamic.new(:action, :default => 'index')]
+ g.result :controller, "::Controllers::ContentController", true
+ go c
+
+ assert_nil execute('boo')
+ assert_nil execute('boo/blah')
+ assert_nil execute('hi/dude/what')
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'index'}, execute('hi'))
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'index'}, execute('hi/index'))
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'dude'}, execute('hi/dude'))
+ end
+
+ def test_dynamic_with_string_condition
+ c = [Static.new("hi"), Dynamic.new(:action, :condition => 'index')]
+ g.result :controller, "::Controllers::ContentController", true
+ go c
+
+ assert_nil execute('boo')
+ assert_nil execute('boo/blah')
+ assert_nil execute('hi')
+ assert_nil execute('hi/dude/what')
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'index'}, execute('hi/index'))
+ assert_nil execute('hi/dude')
end
- def test_defaults_and_restrictions_for_items_not_in_path
- assert_raises(ArgumentError) {route ':year/:name', :requirements => {:year => /\d{4}/}, :defaults => {:name => 'ulysses', :controller => 'content'}, :action => 'show_bio_year'}
- assert_raises(ArgumentError) {route ':year/:name', :requirements => {:year => /\d{4}/, :imagine => /./}, :defaults => {:name => 'ulysses'}, :controller => 'content', :action => 'show_bio_year'}
- end
+ def test_dynamic_with_regexp_condition
+ c = [Static.new("hi"), Dynamic.new(:action, :condition => /^[a-z]+$/)]
+ g.result :controller, "::Controllers::ContentController", true
+ go c
+
+ assert_nil execute('boo')
+ assert_nil execute('boo/blah')
+ assert_nil execute('hi')
+ assert_nil execute('hi/FOXY')
+ assert_nil execute('hi/138708jkhdf')
+ assert_nil execute('hi/dkjfl8792343dfsf')
+ assert_nil execute('hi/dude/what')
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'index'}, execute('hi/index'))
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'dude'}, execute('hi/dude'))
+ end
- def test_optionals_with_regexp
- route ':year/:month/:day', :requirements => {:year => /\d{4}/, :month => /\d{1,2}/, :day => /\d{1,2}/},
- :defaults => {:month => nil, :day => nil},
- :controller => 'content', :action => 'post_by_day'
- verify_recognize('2005/06/12', {:controller => 'content', :action => 'post_by_day', :year => '2005', :month => '06', :day => '12'})
- verify_recognize('2005/06', {:controller => 'content', :action => 'post_by_day', :year => '2005', :month => '06'})
- verify_recognize('2005', {:controller => 'content', :action => 'post_by_day', :year => '2005'})
-
- verify_generate('2005/06/12', {}, {:controller => 'content', :action => 'post_by_day', :year => '2005', :month => '06', :day => '12'}, @defaults)
- verify_generate('2005/06', {}, {:controller => 'content', :action => 'post_by_day', :year => '2005', :month => '06'}, @defaults)
- verify_generate('2005', {}, {:controller => 'content', :action => 'post_by_day', :year => '2005'}, @defaults)
+ def test_dynamic_with_regexp_and_default
+ c = [Static.new("hi"), Dynamic.new(:action, :condition => /^[a-z]+$/, :default => 'index')]
+ g.result :controller, "::Controllers::ContentController", true
+ go c
+
+ assert_nil execute('boo')
+ assert_nil execute('boo/blah')
+ assert_nil execute('hi/FOXY')
+ assert_nil execute('hi/138708jkhdf')
+ assert_nil execute('hi/dkjfl8792343dfsf')
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'index'}, execute('hi'))
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'index'}, execute('hi/index'))
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'dude'}, execute('hi/dude'))
+ assert_nil execute('hi/dude/what')
end
-
- def test_basecamp2
- route 'clients/:client_name/:project_name/', :controller => 'content', :action => 'start_page_redirect'
- verify_recognize('clients/projects/2', {:controller => 'content', :client_name => 'projects', :project_name => '2', :action => 'start_page_redirect'})
+ def test_path
+ c = [Static.new("hi"), Path.new(:file)]
+ g.result :controller, "::Controllers::ContentController", true
+ g.constant_result :action, "download"
+
+ go c
+
+ assert_nil execute('boo')
+ assert_nil execute('boo/blah')
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'download', :file => []}, execute('hi'))
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'download', :file => %w(books agile_rails_dev.pdf)},
+ execute('hi/books/agile_rails_dev.pdf'))
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'download', :file => ['dude']}, execute('hi/dude'))
+ assert_equal 'dude/what', execute('hi/dude/what')[:file].to_s
end
- def test_xal_style_dates
- route 'articles/:category/:year/:month/:day', :controller => 'content', :action => 'list_articles', :category => 'all', :year => nil, :month => nil, :day =>nil
- verify_recognize('articles', {:controller => 'content', :action => 'list_articles', :category => 'all'})
- verify_recognize('articles/porn', {:controller => 'content', :action => 'list_articles', :category => 'porn'})
- verify_recognize('articles/news/2005/08', {:controller => 'content', :action => 'list_articles', :category => 'news', :year => '2005', :month => '08'})
- verify_recognize('articles/news/2005/08/04', {:controller => 'content', :action => 'list_articles', :category => 'news', :year => '2005', :month => '08', :day => '04'})
- assert_equal nil, @route.recognize(%w{articles too many components are here})[0]
- assert_equal nil, @route.recognize('')[0]
-
- verify_generate('articles', {}, {:controller => 'content', :action => 'list_articles'}, @defaults)
- verify_generate('articles', {}, {:controller => 'content', :action => 'list_articles', :category => 'all'}, @defaults)
- verify_generate('articles/news', {}, {:controller => 'content', :action => 'list_articles', :category => 'news'}, @defaults)
- verify_generate('articles/news/2005', {}, {:controller => 'content', :action => 'list_articles', :category => 'news', :year => '2005'}, @defaults)
- verify_generate('articles/news/2005/05', {}, {:controller => 'content', :action => 'list_articles', :category => 'news', :year => '2005', :month => '05'}, @defaults)
- verify_generate('articles/news/2005/05/16', {}, {:controller => 'content', :action => 'list_articles', :category => 'news', :year => '2005', :month => '05', :day => '16'}, @defaults)
-
- assert_equal nil, @route.generate({:controller => 'content', :action => 'list_articles', :day => '2'}, @defaults)[0]
- # The above case should fail because a nil value cannot be present in a path.
- # In other words, since :day is given, :month and :year must be given too.
+ def test_controller
+ c = [Static.new("hi"), Controller.new(:controller)]
+ g.constant_result :action, "hi"
+
+ go c
+
+ assert_nil execute('boo')
+ assert_nil execute('boo/blah')
+ assert_nil execute('hi/x')
+ assert_nil execute('hi/13870948')
+ assert_nil execute('hi/content/dog')
+ assert_nil execute('hi/admin/user/foo')
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'hi'}, execute('hi/content'))
+ assert_equal({:controller => ::Controllers::Admin::UserController, :action => 'hi'}, execute('hi/admin/user'))
end
-
- def test_no_controller
- route 'some/:special/:route', :controller => 'a/missing/controller', :action => 'anything'
- assert_raises(ActionController::RoutingError, "Should raise due to nonexistant controller") {@route.recognize(%w{some matching path})}
- end
- def test_bad_controller_path
- assert_equal nil, @route.recognize(%w{no such controller fake_action id})[0]
- end
- def test_too_short_path
- assert_equal nil, @route.recognize([])[0]
- route 'some/static/route', :controller => 'content', :action => 'show'
- assert_equal nil, route.recognize([])[0]
- end
- def test_too_long_path
- assert_equal nil, @route.recognize(%w{content action id some extra components})[0]
+ def test_standard_route(time = ::RunTimeTests)
+ c = [Controller.new(:controller), Dynamic.new(:action, :default => 'index'), Dynamic.new(:id, :default => nil)]
+ go c
+
+ # Make sure we get the right answers
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'index'}, execute('content'))
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'list'}, execute('content/list'))
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'show', :id => '10'}, execute('content/show/10'))
+
+ assert_equal({:controller => ::Controllers::Admin::UserController, :action => 'index'}, execute('admin/user'))
+ assert_equal({:controller => ::Controllers::Admin::UserController, :action => 'list'}, execute('admin/user/list'))
+ assert_equal({:controller => ::Controllers::Admin::UserController, :action => 'show', :id => 'nseckar'}, execute('admin/user/show/nseckar'))
+
+ assert_nil execute('content/show/10/20')
+ assert_nil execute('food')
+
+ if time
+ source = "def self.execute(path)
+ path = path.split('/') if path.is_a? String
+ index = 0
+ r = #{g.to_s}
+ end"
+ eval(source)
+
+ GC.start
+ n = 1000
+ time = Benchmark.realtime do n.times {
+ execute('content')
+ execute('content/list')
+ execute('content/show/10')
+
+ execute('admin/user')
+ execute('admin/user/list')
+ execute('admin/user/show/nseckar')
+
+ execute('admin/user/show/nseckar/dude')
+ execute('admin/why/show/nseckar')
+ execute('content/show/10/20')
+ execute('food')
+ } end
+ time -= Benchmark.realtime do n.times { } end
+
+
+ puts "\n\nRecognition:"
+ per_url = time / (n * 10)
+
+ puts "#{per_url * 1000} ms/url"
+ puts "#{1 / per_url} urls/s\n\n"
+ end
end
- def test_incorrect_static_component
- route 'some/static/route', :controller => 'content', :action => 'show'
- assert_equal nil, route.recognize(%w{an non_matching path})[0]
+
+ def test_default_route
+ g.result :controller, "::Controllers::ContentController", true
+ g.constant_result :action, 'index'
+
+ go []
+
+ assert_nil execute('x')
+ assert_nil execute('hello/world/how')
+ assert_nil execute('hello/world/how/are')
+ assert_nil execute('hello/world/how/are/you/today')
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'index'}, execute([]))
end
- def test_no_controller_defined
- route 'some/:path/:without/a/controller'
- assert_equal nil, route.recognize(%w{some matching path a controller})[0]
+end
+
+class GenerationTests < Test::Unit::TestCase
+ attr_accessor :generator
+ alias :g :generator
+ def setup
+ @generator = GenerationGenerator.new # ha!
end
- def test_mismatching_requirements
- route 'some/path', :controller => 'content', :action => 'fish'
- assert_equal nil, route.generate({:controller => 'admin/user', :action => 'list'})[0]
- assert_equal nil, route.generate({:controller => 'content', :action => 'list'})[0]
- assert_equal nil, route.generate({:controller => 'admin/user', :action => 'fish'})[0]
+ def go(components)
+ g.current = components.first
+ g.after = components[1..-1] || []
+ g.go
end
- def test_missing_value_for_generate
- assert_equal nil, route.generate({})[0] # :controller is missing
- end
- def test_nils_inside_generated_path
- route 'show/:year/:month/:day', :month => nil, :day => nil, :controller => 'content', :action => 'by_date'
- assert_equal nil, route.generate({:year => 2005, :day => 10})[0]
+ def execute(options, recall, show = false)
+ source = "\n
+expire_on = ::ActionController::Routing.expiry_hash(options, recall)
+hash = merged = recall.merge(options)
+not_expired = true
+
+#{g.to_s}\n\n"
+ puts source if show
+ eval(source)
end
- def test_expand_controller_path_non_nested_no_leftover
- controller, leftovers = @route.send :eat_path_to_controller, %w{content}
- assert_equal Controllers::ContentController, controller
- assert_equal [], leftovers
- end
- def test_expand_controller_path_non_nested_with_leftover
- controller, leftovers = @route.send :eat_path_to_controller, %w{content action id}
- assert_equal Controllers::ContentController, controller
- assert_equal %w{action id}, leftovers
- end
- def test_expand_controller_path_nested_no_leftover
- controller, leftovers = @route.send :eat_path_to_controller, %w{admin user}
- assert_equal Controllers::Admin::UserController, controller
- assert_equal [], leftovers
- end
- def test_expand_controller_path_nested_no_leftover
- controller, leftovers = @route.send :eat_path_to_controller, %w{admin user action id}
- assert_equal Controllers::Admin::UserController, controller
- assert_equal %w{action id}, leftovers
- end
-
- def test_path_collection
- route '*path_info', :controller => 'content', :action => 'fish'
- verify_recognize'path/with/slashes',
- :controller => 'content', :action => 'fish', :path_info => %w(path with slashes)
- verify_generate('path/with/slashes', {},
- {:controller => 'content', :action => 'fish', :path_info => 'path/with/slashes'},
- {})
- end
- def test_path_collection_with_array
- route '*path_info', :controller => 'content', :action => 'fish'
- verify_recognize'path/with/slashes',
- :controller => 'content', :action => 'fish', :path_info => %w(path with slashes)
- verify_generate('path/with/slashes', {},
- {:controller => 'content', :action => 'fish', :path_info => %w(path with slashes)},
- {})
- end
-
- def test_path_empty_list
- route '*a', :controller => 'content'
- verify_recognize '', :controller => 'content', :a => []
- end
+ Static = ::ActionController::Routing::StaticComponent
+ Dynamic = ::ActionController::Routing::DynamicComponent
+ Path = ::ActionController::Routing::PathComponent
+ Controller = ::ActionController::Routing::ControllerComponent
- def test_special_characters
- route ':id', :controller => 'content', :action => 'fish'
- verify_recognize'id+with+spaces',
- :controller => 'content', :action => 'fish', :id => 'id with spaces'
- verify_generate('id+with+spaces', {},
- {:controller => 'content', :action => 'fish', :id => 'id with spaces'}, {})
- verify_recognize 'id%2Fwith%2Fslashes',
- :controller => 'content', :action => 'fish', :id => 'id/with/slashes'
- verify_generate('id%2Fwith%2Fslashes', {},
- {:controller => 'content', :action => 'fish', :id => 'id/with/slashes'}, {})
- end
-
- def test_generate_with_numeric_param
- o = Object.new
- def o.to_param() 10 end
- verify_generate('content/action/10', {}, {:controller => 'content', :action => 'action', :id => o}, @defaults)
- verify_generate('content/show/10', {}, {:controller => 'content', :action => 'show', :id => o}, @defaults)
- end
-end
-
-class RouteSetTests < Test::Unit::TestCase
- def setup
- @set = ActionController::Routing::RouteSet.new
- @rails_route = ActionController::Routing::Route.new '/:controller/:action/:id', :action => 'index', :id => nil
- @request = ActionController::TestRequest.new({}, {}, nil)
- end
- def test_emptyness
- assert_equal true, @set.empty?, "New RouteSets should respond to empty? with true."
- @set.each { flunk "New RouteSets should be empty." }
- end
- def test_add_illegal_route
- assert_raises(TypeError) {@set.add_route "I'm not actually a route."}
- end
- def test_add_normal_route
- @set.add_route @rails_route
- seen = false
- @set.each do |route|
- assert_equal @rails_route, route
- flunk("Each should have yielded only a single route!") if seen
- seen = true
- end
+ def test_all_static_no_requirements
+ c = [Static.new("hello"), Static.new("world")]
+ go c
+
+ assert_equal "hello/world", execute({}, {})
end
- def test_expand_controller_path_non_relative
- defaults = {:controller => 'admin/user', :action => 'list'}
- options = {:controller => '/content'}
- @set.expand_controller_path!(options, defaults)
- assert_equal({:controller => 'content'}, options)
- end
- def test_expand_controller_path_relative_to_nested
- defaults = {:controller => 'admin/user', :action => 'list'}
- options = {:controller => 'access'}
- @set.expand_controller_path!(options, defaults)
- assert_equal({:controller => 'admin/access'}, options)
- end
- def test_expand_controller_path_relative_to_root
- defaults = {:controller => 'content', :action => 'list'}
- options = {:controller => 'resource'}
- @set.expand_controller_path!(options, defaults)
- assert_equal({:controller => 'resource'}, options)
- end
- def test_expand_controller_path_into_module
- defaults = {:controller => 'content', :action => 'list'}
- options = {:controller => 'admin/user'}
- @set.expand_controller_path!(options, defaults)
- assert_equal({:controller => 'admin/user'}, options)
- end
- def test_expand_controller_path_switch_module_with_absolute
- defaults = {:controller => 'user/news', :action => 'list'}
- options = {:controller => '/admin/user'}
- @set.expand_controller_path!(options, defaults)
- assert_equal({:controller => 'admin/user'}, options)
- end
- def test_expand_controller_no_default
- options = {:controller => 'content'}
- @set.expand_controller_path!(options, {})
- assert_equal({:controller => 'content'}, options)
+ def test_basic_dynamic
+ c = [Static.new("hi"), Dynamic.new(:action)]
+ go c
+
+ assert_equal 'hi/index', execute({:action => 'index'}, {:action => 'index'})
+ assert_equal 'hi/show', execute({:action => 'show'}, {:action => 'index'})
+ assert_equal 'hi/list+people', execute({}, {:action => 'list people'})
+ assert_nil execute({},{})
end
- # Don't put a leading / on the url.
- # Make sure the controller is one from the above fake Controllers module.
- def verify_recognize(expected_controller, expected_path_parameters=nil, path=nil)
- @set.add_route(@rails_route) if @set.empty?
- @request.path = path if path
- controller = @set.recognize!(@request)
- assert_equal expected_controller, controller
- assert_equal expected_path_parameters, @request.path_parameters if expected_path_parameters
+ def test_dynamic_with_default
+ c = [Static.new("hi"), Dynamic.new(:action, :default => 'index')]
+ go c
+
+ assert_equal 'hi', execute({:action => 'index'}, {:action => 'index'})
+ assert_equal 'hi/show', execute({:action => 'show'}, {:action => 'index'})
+ assert_equal 'hi/list+people', execute({}, {:action => 'list people'})
+ assert_equal 'hi', execute({}, {})
end
- # The expected url should not have a leading /
- # You can use @defaults if you want a set of plausible defaults
- def verify_generate(expected_url, options, expected_extras={})
- @set.add_route(@rails_route) if @set.empty?
- components, extras = @set.generate(options, @request)
- assert_equal expected_extras, extras, "#incorrect extra's"
- assert_equal expected_url, components.join('/'), "incorrect url"
+ def test_dynamic_with_regexp_condition
+ c = [Static.new("hi"), Dynamic.new(:action, :condition => /^[a-z]+$/)]
+ go c
+
+ assert_equal 'hi/index', execute({:action => 'index'}, {:action => 'index'})
+ assert_nil execute({:action => 'fox5'}, {:action => 'index'})
+ assert_nil execute({:action => 'something_is_up'}, {:action => 'index'})
+ assert_nil execute({}, {:action => 'list people'})
+ assert_equal 'hi/abunchofcharacter', execute({:action => 'abunchofcharacter'}, {})
+ assert_nil execute({}, {})
end
- def typical_request
- @request.path_parameters = {:controller => 'content', :action => 'show', :id => '10'}
+
+ def test_dynamic_with_default_and_regexp_condition
+ c = [Static.new("hi"), Dynamic.new(:action, :default => 'index', :condition => /^[a-z]+$/)]
+ go c
+
+ assert_equal 'hi', execute({:action => 'index'}, {:action => 'index'})
+ assert_nil execute({:action => 'fox5'}, {:action => 'index'})
+ assert_nil execute({:action => 'something_is_up'}, {:action => 'index'})
+ assert_nil execute({}, {:action => 'list people'})
+ assert_equal 'hi/abunchofcharacter', execute({:action => 'abunchofcharacter'}, {})
+ assert_equal 'hi', execute({}, {})
end
- def typical_nested_request
- @request.path_parameters = {:controller => 'admin/user', :action => 'grant', :id => '02seckar'}
+
+ def test_path
+ c = [Static.new("hi"), Path.new(:file)]
+ go c
+
+ assert_equal 'hi', execute({:file => []}, {})
+ assert_equal 'hi/books/agile_rails_dev.pdf', execute({:file => %w(books agile_rails_dev.pdf)}, {})
+ assert_equal 'hi/books/development%26whatever/agile_rails_dev.pdf', execute({:file => %w(books development&whatever agile_rails_dev.pdf)}, {})
+
+ assert_equal 'hi', execute({:file => ''}, {})
+ assert_equal 'hi/books/agile_rails_dev.pdf', execute({:file => 'books/agile_rails_dev.pdf'}, {})
+ assert_equal 'hi/books/development%26whatever/agile_rails_dev.pdf', execute({:file => 'books/development&whatever/agile_rails_dev.pdf'}, {})
end
- def test_generate_typical_controller_action_path
- typical_request
- verify_generate('content/list', {:controller => 'content', :action => 'list'})
+ def test_controller
+ c = [Static.new("hi"), Controller.new(:controller)]
+ go c
+
+ assert_nil execute({}, {})
+ assert_equal 'hi/content', execute({:controller => 'content'}, {})
+ assert_equal 'hi/admin/user', execute({:controller => 'admin/user'}, {})
+ assert_equal 'hi/content', execute({}, {:controller => 'content'})
+ assert_equal 'hi/admin/user', execute({}, {:controller => 'admin/user'})
end
- def test_generate_typical_controller_index_path_explicit_index
- typical_request
- verify_generate('content', {:controller => 'content', :action => 'index'})
+
+ def test_standard_route(time = ::RunTimeTests)
+ c = [Controller.new(:controller), Dynamic.new(:action, :default => 'index'), Dynamic.new(:id, :default => nil)]
+ go c
+
+ # Make sure we get the right answers
+ assert_equal('content', execute({:action => 'index'}, {:controller => 'content', :action => 'list'}))
+ assert_equal('content/list', execute({:action => 'list'}, {:controller => 'content', :action => 'index'}))
+ assert_equal('content/show/10', execute({:action => 'show', :id => '10'}, {:controller => 'content', :action => 'list'}))
+
+ assert_equal('admin/user', execute({:action => 'index'}, {:controller => 'admin/user', :action => 'list'}))
+ assert_equal('admin/user/list', execute({:action => 'list'}, {:controller => 'admin/user', :action => 'index'}))
+ assert_equal('admin/user/show/10', execute({:action => 'show', :id => '10'}, {:controller => 'admin/user', :action => 'list'}))
+
+ if time
+ GC.start
+ n = 1000
+ time = Benchmark.realtime do n.times {
+ execute({:action => 'index'}, {:controller => 'content', :action => 'list'})
+ execute({:action => 'list'}, {:controller => 'content', :action => 'index'})
+ execute({:action => 'show', :id => '10'}, {:controller => 'content', :action => 'list'})
+
+ execute({:action => 'index'}, {:controller => 'admin/user', :action => 'list'})
+ execute({:action => 'list'}, {:controller => 'admin/user', :action => 'index'})
+ execute({:action => 'show', :id => '10'}, {:controller => 'admin/user', :action => 'list'})
+ } end
+ time -= Benchmark.realtime do n.times { } end
+
+ puts "\n\nGeneration:"
+ per_url = time / (n * 6)
+
+ puts "#{per_url * 1000} ms/url"
+ puts "#{1 / per_url} urls/s\n\n"
+ end
end
- def test_generate_typical_controller_index_path_explicit_index
- typical_request
- verify_generate('content', {:controller => 'content', :action => 'index'})
+
+ def test_default_route
+ g.if(g.check_conditions(:controller => 'content', :action => 'welcome')) { go [] }
+
+ assert_nil execute({:controller => 'foo', :action => 'welcome'}, {})
+ assert_nil execute({:controller => 'content', :action => 'elcome'}, {})
+ assert_nil execute({:action => 'elcome'}, {:controller => 'content'})
+
+ assert_equal '', execute({:controller => 'content', :action => 'welcome'}, {})
+ assert_equal '', execute({:action => 'welcome'}, {:controller => 'content'})
+ assert_equal '', execute({:action => 'welcome', :id => '10'}, {:controller => 'content'})
end
- def test_generate_typical_controller_index_path_implicit_index
- typical_request
- @request.path_parameters[:controller] = 'resource'
- verify_generate('content', {:controller => 'content'})
+end
+
+class RouteTests < Test::Unit::TestCase
+ def route(*args)
+ @route = ::ActionController::Routing::Route.new(*args) unless args.empty?
+ return @route
end
- def test_generate_no_perfect_route
- typical_request
- verify_generate('admin/user/show/43seckar', {:controller => 'admin/user', :action => 'show', :id => '43seckar', :likes_fishing => 'fuzzy(0.3)'}, {:likes_fishing => 'fuzzy(0.3)'})
+ def rec(path, show = false)
+ path = path.split('/') if path.is_a? String
+ index = 0
+ source = route.write_recognition.to_s
+ puts "\n\n#{source}\n\n" if show
+ r = eval(source)
+ r ? r.symbolize_keys : r
+ end
+ def gen(options, recall = nil, show = false)
+ recall ||= options.dup
+
+ expire_on = ::ActionController::Routing.expiry_hash(options, recall)
+ hash = merged = recall.merge(options)
+ not_expired = true
+
+ source = route.write_generation.to_s
+ puts "\n\n#{source}\n\n" if show
+ eval(source)
+
end
- def test_generate_no_match
- @set.add_route(@rails_route)
- @request.path_parameters = {}
- assert_raises(ActionController::RoutingError) {@set.generate({}, @request)}
+ def test_static
+ route 'hello/world', :known => 'known_value'
+
+ assert_nil rec('hello/turn')
+ assert_nil rec('turn/world')
+ assert_equal({:known => 'known_value'}, rec('hello/world'))
+
+ assert_nil gen(:known => 'foo')
+ assert_nil gen({})
+ assert_equal 'hello/world', gen(:known => 'known_value')
+ assert_equal 'hello/world', gen(:known => 'known_value', :extra => 'hi')
+ assert_equal [:extra], route.extra_keys(:known => 'known_value', :extra => 'hi')
end
- def test_encoded_strings
- verify_recognize(Controllers::Admin::UserController, {:controller => 'admin/user', :action => 'info', :id => "Nicholas Seckar"}, path='/admin/user/info/Nicholas%20Seckar')
+ def test_dynamic
+ route 'hello/:name', :controller => 'content', :action => 'show_person'
+
+ assert_nil rec('hello')
+ assert_nil rec('foo/bar')
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'show_person', :name => 'rails'}, rec('hello/rails'))
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'show_person', :name => 'Nicholas Seckar'}, rec('hello/Nicholas+Seckar'))
+
+ assert_nil gen(:controller => 'content', :action => 'show_dude', :name => 'rails')
+ assert_nil gen(:controller => 'content', :action => 'show_person')
+ assert_nil gen(:controller => 'admin/user', :action => 'show_person', :name => 'rails')
+ assert_equal 'hello/rails', gen(:controller => 'content', :action => 'show_person', :name => 'rails')
+ assert_equal 'hello/Nicholas+Seckar', gen(:controller => 'content', :action => 'show_person', :name => 'Nicholas Seckar')
end
- def test_action_dropped_when_controller_changes
- @request.path_parameters = {:controller => 'content', :action => 'list'}
- options = {:controller => 'resource'}
- @set.connect ':action/:controller'
- verify_generate('index/resource', options)
+ def test_typical
+ route ':controller/:action/:id', :action => 'index', :id => nil
+ assert_nil rec('hello')
+ assert_nil rec('foo bar')
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'index'}, rec('content'))
+ assert_equal({:controller => ::Controllers::Admin::UserController, :action => 'index'}, rec('admin/user'))
+
+ assert_equal({:controller => ::Controllers::Admin::UserController, :action => 'index'}, rec('admin/user/index'))
+ assert_equal({:controller => ::Controllers::Admin::UserController, :action => 'list'}, rec('admin/user/list'))
+ assert_equal({:controller => ::Controllers::Admin::UserController, :action => 'show', :id => '10'}, rec('admin/user/show/10'))
+
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'list'}, rec('content/list'))
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'show', :id => '10'}, rec('content/show/10'))
+
+
+ assert_equal 'content', gen(:controller => 'content', :action => 'index')
+ assert_equal 'content/list', gen(:controller => 'content', :action => 'list')
+ assert_equal 'content/show/10', gen(:controller => 'content', :action => 'show', :id => '10')
+
+ assert_equal 'admin/user', gen(:controller => 'admin/user', :action => 'index')
+ assert_equal 'admin/user', gen(:controller => 'admin/user')
+ assert_equal 'admin/user', gen({:controller => 'admin/user'}, {:controller => 'content', :action => 'list', :id => '10'})
+ assert_equal 'admin/user/show/10', gen(:controller => 'admin/user', :action => 'show', :id => '10')
end
+end
- def test_action_dropped_when_controller_given
- @request.path_parameters = {:controller => 'content', :action => 'list'}
- options = {:controller => 'content'}
- @set.connect ':action/:controller'
- verify_generate('index/content', options)
+class RouteSetTests < Test::Unit::TestCase
+ attr_reader :rs
+ def setup
+ @rs = ::ActionController::Routing::RouteSet.new
+ @rs.draw {|m| m.connect ':controller/:action/:id' }
end
- def test_default_dropped_with_nil_option
- @request.path_parameters = {:controller => 'content', :action => 'action', :id => '10'}
- verify_generate 'content/action', {:id => nil}
- end
-
- def test_url_to_self
- @request.path_parameters = {:controller => 'admin/users', :action => 'index'}
- verify_generate 'admin/users', {}
+ def test_default_setup
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'index'}.stringify_keys, rs.recognize_path(%w(content)))
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'list'}.stringify_keys, rs.recognize_path(%w(content list)))
+ assert_equal({:controller => ::Controllers::ContentController, :action => 'show', :id => '10'}.stringify_keys, rs.recognize_path(%w(content show 10)))
+
+ assert_equal({:controller => ::Controllers::Admin::UserController, :action => 'show', :id => '10'}.stringify_keys, rs.recognize_path(%w(admin user show 10)))
+
+ assert_equal ['admin/user/show/10', {}], rs.generate({:controller => 'admin/user', :action => 'show', :id => 10})
+
+ assert_equal ['admin/user/show', {}], rs.generate({:action => 'show'}, {:controller => 'admin/user', :action => 'list', :id => '10'})
+ assert_equal ['admin/user/list/10', {}], rs.generate({}, {:controller => 'admin/user', :action => 'list', :id => '10'})
end
-
- def test_url_with_spaces_in_controller
- @request.path = 'not%20a%20valid/controller/name'
- @set.add_route(@rails_route) if @set.empty?
- assert_raises(ActionController::RoutingError) {@set.recognize!(@request)}
+
+ def test_time_recognition
+ n = 10000
+ if RunTimeTests
+ GC.start
+ rectime = Benchmark.realtime do
+ n.times do
+ rs.recognize_path(%w(content))
+ rs.recognize_path(%w(content list))
+ rs.recognize_path(%w(content show 10))
+ rs.recognize_path(%w(admin user))
+ rs.recognize_path(%w(admin user list))
+ rs.recognize_path(%w(admin user show 10))
+ end
+ end
+ puts "\n\nRecognition (RouteSet):"
+ per_url = rectime / (n * 6)
+ puts "#{per_url * 1000} ms/url"
+ puts "#{1 / per_url} url/s\n\n"
+ end
end
- def test_url_with_dots_in_controller
- @request.path = 'not.valid/controller/name'
- @set.add_route(@rails_route) if @set.empty?
- assert_raises(ActionController::RoutingError) {@set.recognize!(@request)}
+ def test_time_generation
+ n = 5000
+ if RunTimeTests
+ GC.start
+ pairs = [
+ [{:controller => 'content', :action => 'index'}, {:controller => 'content', :action => 'show'}],
+ [{:controller => 'content'}, {:controller => 'content', :action => 'index'}],
+ [{:controller => 'content', :action => 'list'}, {:controller => 'content', :action => 'index'}],
+ [{:controller => 'content', :action => 'show', :id => '10'}, {:controller => 'content', :action => 'list'}],
+ [{:controller => 'admin/user', :action => 'index'}, {:controller => 'admin/user', :action => 'show'}],
+ [{:controller => 'admin/user'}, {:controller => 'admin/user', :action => 'index'}],
+ [{:controller => 'admin/user', :action => 'list'}, {:controller => 'admin/user', :action => 'index'}],
+ [{:controller => 'admin/user', :action => 'show', :id => '10'}, {:controller => 'admin/user', :action => 'list'}],
+ ]
+ p = nil
+ gentime = Benchmark.realtime do
+ n.times do
+ pairs.each {|(a, b)| rs.generate(a, b)}
+ end
+ end
+
+ puts "\n\nGeneration (RouteSet): (#{(n * 8)} urls)"
+ per_url = gentime / (n * 8)
+ puts "#{per_url * 1000} ms/url"
+ puts "#{1 / per_url} url/s\n\n"
+ end
end
- def test_generate_of_empty_url
- @set.connect '', :controller => 'content', :action => 'view', :id => "1"
- @set.add_route(@rails_route)
- verify_generate('content/view/2', {:controller => 'content', :action => 'view', :id => 2})
- verify_generate('', {:controller => 'content', :action => 'view', :id => 1})
+ def test_basic_named_route
+ rs.home '', :controller => 'content', :action => 'list'
+ x = setup_for_named_route
+ assert_equal({:controller => 'content', :action => 'list'},
+ x.new.send(:home_url))
end
- def test_generate_of_empty_url_with_numeric_requirement
- @set.connect '', :controller => 'content', :action => 'view', :id => 1
- @set.add_route(@rails_route)
- verify_generate('content/view/2', {:controller => 'content', :action => 'view', :id => 2})
- verify_generate('', {:controller => 'content', :action => 'view', :id => 1})
+
+ def test_named_route_with_option
+ rs.page 'page/:title', :controller => 'content', :action => 'show_page'
+ x = setup_for_named_route
+ assert_equal({:controller => 'content', :action => 'show_page', :title => 'new stuff'},
+ x.new.send(:page_url, :title => 'new stuff'))
+ end
+
+ def setup_for_named_route
+ x = Class.new
+ x.send(:define_method, :url_for) {|x| x}
+ x.send :include, ::ActionController::Routing::NamedRoutes
+ x
end
end
-#require '../assertions/action_pack_assertions.rb'
-class AssertionRoutingTests < Test::Unit::TestCase
- def test_assert_routing
- ActionController::Routing::Routes.reload rescue nil
- assert_routing('content', {:controller => 'content', :action => 'index'})
- end
end