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 - 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)) + raise ArgumentError, "Valid criteria are strings, regular expressions, true, or nil" 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| + end - if item.kind_of? Symbol - collection = false + class Component #:nodoc + def dynamic?() false end + def optional?() false end - if /^\*/ =~ item.to_s - collection = true - item = item.to_s.sub(/^\*/,"").intern - 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 + end + end - used_names[item] = true - value = options[item] || defaults[item] || @defaults[item] - return nil, requirements_for(item) unless passes_requirements?(item, value) + class StaticComponent < Component #:nodoc + attr_reader :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 - 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 + def write_generation(g) + g.add_segment(value) {|gp| gp.continue } + end + end + + class DynamicComponent < Component #:nodoc + attr_reader :key, :default + attr_accessor :condition + + def dynamic?() true end + def optional?() @optional end + + 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 + + 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 - - @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 + + 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 - @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 + + 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 - 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 - + + 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 - # 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] - end - return nil, nil # Path ended, but no controller found. + + 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? - 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. + 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 - 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 = [] - 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] + 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 + + 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 - - return selected[1..-1] unless selected.nil? - raise RoutingError.new("Generation failure: No route for url_options #{options.inspect}, defaults: #{defaults.inspect}", failures) end - - # Recognize the provided path. - # Raise RoutingError if the path can't be recognized. - def recognize!(request) - path = ((%r{^/?(.*)/?$} =~ request.path) ? $1 : request.path).split('/') - raise RoutingError, "There are no routes defined!" if @routes.empty? - - failures = [] - self.each do |route| - controller, options = route.recognize(path) - if controller.nil? - failures << [route, options] if ActionController::Base.debug_routes - else - 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 - 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' - end - module Editing - PageController = FakeController.new 'Editing::PageController', 'editing/page' - ImageController = FakeController.new 'Editing::ImageController', 'editing/image' - end - module User - NewsController = FakeController.new 'User::NewsController', 'user/news' - PaymentController = FakeController.new 'User::PaymentController', 'user/payment' - end - ContentController = FakeController.new 'ContentController', 'content' - ResourceController = FakeController.new 'ResourceController', 'resource' -end +module ActionController::CodeGeneration -# 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 +class SourceTests < Test::Unit::TestCase + attr_accessor :source + def setup + @source = Source.new + end + def test_initial_state + assert_equal [], source.lines + assert_equal 0, source.indentation_level + end + + 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 + + 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" + + assert_equal text, source.to_s + end +end + +class CodeGeneratorTests < Test::Unit::TestCase + attr_accessor :generator + def setup + @generator = CodeGenerator.new + end + + def test_initial_state + assert_equal [], generator.source.lines + assert_equal [], generator.locals + end + + 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_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 + + 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_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 +end + +# XXX Extract to test/controller/fake_controllers.rb +module Object::Controllers + def self.const_available?(*args) + const_defined?(*args) + end + + class ContentController + end + module Admin + def self.const_available?(*args) + const_defined?(*args) + end + + class UserController + end + end +end + + +class RecognitionTests < Test::Unit::TestCase + attr_accessor :generator + alias :g :generator + def setup + @generator = RecognitionGenerator.new + end + + def go(components) + g.current = components.first + g.after = components[1..-1] || [] + g.go + end + + 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 + + Static = ::ActionController::Routing::StaticComponent + Dynamic = ::ActionController::Routing::DynamicComponent + Path = ::ActionController::Routing::PathComponent + Controller = ::ActionController::Routing::ControllerComponent + + 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_basic_dynamic + c = [Static.new("hi"), Dynamic.new(:action)] + 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 => '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_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_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_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_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_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_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 +end + +class GenerationTests < Test::Unit::TestCase + attr_accessor :generator + alias :g :generator + def setup + @generator = GenerationGenerator.new # ha! + end + + def go(components) + g.current = components.first + g.after = components[1..-1] || [] + g.go + end + + 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 + + Static = ::ActionController::Routing::StaticComponent + Dynamic = ::ActionController::Routing::DynamicComponent + Path = ::ActionController::Routing::PathComponent + Controller = ::ActionController::Routing::ControllerComponent + + def test_all_static_no_requirements + c = [Static.new("hello"), Static.new("world")] + go c + + assert_equal "hello/world", execute({}, {}) + end + + 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 + + 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 + + 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 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 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_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_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_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 +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) + @route = ::ActionController::Routing::Route.new(*args) unless args.empty? return @route end - - def setup - self.route '/:controller/:action/:id' - @defaults = {:controller => 'content', :action => 'show', :id => '314'} - 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 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 - - # 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" - 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) - 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'}) - 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) - 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 - - 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) - 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) - 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) - 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) - 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) - 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' + def gen(options, recall = nil, show = false) + recall ||= options.dup - 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) + expire_on = ::ActionController::Routing.expiry_hash(options, recall) + hash = merged = recall.merge(options) + not_expired = true - # 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 - - def test_basecamp - route 'clients/', :controller => 'content' - verify_generate('clients', {}, {:controller => 'content'}, {}) # Would like to have clients/ - verify_generate('clients', {}, {:controller => 'content'}, @defaults) - 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] + source = route.write_generation.to_s + puts "\n\n#{source}\n\n" if show + eval(source) - 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'}) - 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_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'}) + def test_static + route 'hello/world', :known => 'known_value' - 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) - 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'}) - 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] + assert_nil rec('hello/turn') + assert_nil rec('turn/world') + assert_equal({:known => 'known_value'}, rec('hello/world')) - 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. + 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_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] - 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] - end - def test_no_controller_defined - route 'some/:path/:without/a/controller' - assert_equal nil, route.recognize(%w{some matching path a controller})[0] + 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_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] - 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] - 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 - - 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) + 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 class RouteSetTests < Test::Unit::TestCase + attr_reader :rs 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) + @rs = ::ActionController::Routing::RouteSet.new + @rs.draw {|m| m.connect ':controller/:action/:id' } 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." } + + 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_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 + + 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_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) - 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 - 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" - end - def typical_request - @request.path_parameters = {:controller => 'content', :action => 'show', :id => '10'} - end - def typical_nested_request - @request.path_parameters = {:controller => 'admin/user', :action => 'grant', :id => '02seckar'} - end - - def test_generate_typical_controller_action_path - typical_request - verify_generate('content/list', {:controller => 'content', :action => 'list'}) - end - def test_generate_typical_controller_index_path_explicit_index - typical_request - verify_generate('content', {:controller => 'content', :action => 'index'}) - end - def test_generate_typical_controller_index_path_explicit_index - typical_request - verify_generate('content', {:controller => 'content', :action => 'index'}) - end - def test_generate_typical_controller_index_path_implicit_index - typical_request - @request.path_parameters[:controller] = 'resource' - verify_generate('content', {:controller => 'content'}) - 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)'}) - end - - def test_generate_no_match - @set.add_route(@rails_route) - @request.path_parameters = {} - assert_raises(ActionController::RoutingError) {@set.generate({}, @request)} - end - - def test_encoded_strings - verify_recognize(Controllers::Admin::UserController, {:controller => 'admin/user', :action => 'info', :id => "Nicholas Seckar"}, path='/admin/user/info/Nicholas%20Seckar') - 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_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_action_dropped_when_controller_given - @request.path_parameters = {:controller => 'content', :action => 'list'} - options = {:controller => 'content'} - @set.connect ':action/:controller' - verify_generate('index/content', options) - end - - def test_default_dropped_with_nil_option - @request.path_parameters = {:controller => 'content', :action => 'action', :id => '10'} - verify_generate 'content/action', {:id => nil} + 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_url_to_self - @request.path_parameters = {:controller => 'admin/users', :action => 'index'} - verify_generate 'admin/users', {} - end + 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 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)} - 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)} - 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}) - 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 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 diff --git a/activesupport/lib/active_support/core_ext/array.rb b/activesupport/lib/active_support/core_ext/array.rb new file mode 100644 index 0000000000..83eef79b38 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/array.rb @@ -0,0 +1,5 @@ +require File.dirname(__FILE__) + '/array/to_param' + +class Array #:nodoc: + include ActiveSupport::CoreExtensions::Array::ToParam +end diff --git a/activesupport/lib/active_support/core_ext/array/to_param.rb b/activesupport/lib/active_support/core_ext/array/to_param.rb new file mode 100644 index 0000000000..85e91e6b1a --- /dev/null +++ b/activesupport/lib/active_support/core_ext/array/to_param.rb @@ -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 diff --git a/activesupport/lib/active_support/core_ext/cgi.rb b/activesupport/lib/active_support/core_ext/cgi.rb new file mode 100644 index 0000000000..2378297b7d --- /dev/null +++ b/activesupport/lib/active_support/core_ext/cgi.rb @@ -0,0 +1,5 @@ +require File.dirname(__FILE__) + '/cgi/escape_skipping_slashes' + +class CGI + extend(ActiveSupport::CoreExtensions::CGI::EscapeSkippingSlashes) +end diff --git a/activesupport/lib/active_support/core_ext/cgi/escape_skipping_slashes.rb b/activesupport/lib/active_support/core_ext/cgi/escape_skipping_slashes.rb new file mode 100644 index 0000000000..a21e98fa80 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/cgi/escape_skipping_slashes.rb @@ -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 diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index dd210a8ebd..ef5fd0ce1f 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -14,7 +14,7 @@ module Dependencies end def depend_on(file_name, swallow_load_errors = false) - if !loaded.include?(file_name) + unless loaded.include?(file_name) loaded << file_name begin @@ -34,7 +34,7 @@ module Dependencies end 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) end @@ -52,8 +52,10 @@ module Dependencies attr_reader :path attr_reader :root - def self.root(*load_paths) - RootLoadingModule.new(*load_paths) + class << self + def root(*load_paths) + RootLoadingModule.new(*load_paths) + end end def initialize(root, path=[]) @@ -61,7 +63,7 @@ module Dependencies @root = root end - def root?() self.root == self end + def root?() self.root == self end def load_paths() self.root.load_paths end # Load missing constants if possible. @@ -71,13 +73,15 @@ module Dependencies # Load the controller class or a parent module. def const_load!(name, file_name = nil) + file_name ||= 'application' if root? && name.to_s == 'ApplicationController' path = self.path + [file_name || name] load_paths.each do |load_path| fs_path = load_path.filesystem_path(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]) self.const_set name, new_module if self.root? @@ -88,7 +92,7 @@ module Dependencies Object.const_set(name, new_module) end break - elsif File.file?(fs_path) + when File.file?(fs_path) self.root.load_file!(fs_path) # Import the loaded constant from Object provided we are the root node. @@ -97,7 +101,7 @@ module Dependencies end end - return self.const_defined?(name) + self.const_defined?(name) end # 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. def filesystem_path(path, allow_module=true) 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 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)) - return File.file?(result) ? result : nil + File.file?(result) ? result : nil end def const_name_to_file_name(name) @@ -164,8 +168,8 @@ module Dependencies 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_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_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) class Module #:nodoc: # Use const_missing to autoload associations so we don't have to @@ -186,19 +190,17 @@ end class Object #:nodoc: def load(file, *extras) - begin super(file, *extras) - rescue Object => exception - exception.blame_file! file - raise - end + super(file, *extras) + rescue Object => exception + exception.blame_file! file + raise end def require(file, *extras) - begin super(file, *extras) - rescue Object => exception - exception.blame_file! file - raise - end + super(file, *extras) + rescue Object => exception + exception.blame_file! file + raise end end diff --git a/activesupport/test/core_ext/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb new file mode 100644 index 0000000000..3b16c8d2b3 --- /dev/null +++ b/activesupport/test/core_ext/array_ext_test.rb @@ -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 diff --git a/activesupport/test/core_ext/cgi_ext_test.rb b/activesupport/test/core_ext/cgi_ext_test.rb new file mode 100644 index 0000000000..ec9bb41334 --- /dev/null +++ b/activesupport/test/core_ext/cgi_ext_test.rb @@ -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