From b20c575ac02373723438468932ceddd97056c9ec Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Thu, 1 Jun 2006 15:42:08 +0000 Subject: New routes implementation. Simpler, faster, easier to understand. The published API for config/routes.rb is unchanged, but nearly everything else is different, so expect breakage in plugins and libs that try to fiddle with routes. git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4394 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- actionpack/CHANGELOG | 7 +- actionpack/lib/action_controller/assertions.rb | 5 +- actionpack/lib/action_controller/base.rb | 1 - .../lib/action_controller/code_generation.rb | 243 --- actionpack/lib/action_controller/routing.rb | 1471 +++++++++------- actionpack/lib/action_controller/test_process.rb | 4 +- actionpack/lib/action_controller/url_rewriter.rb | 29 +- .../test/controller/action_pack_assertions_test.rb | 16 +- .../app/controllers/admin/user_controller.rb | 0 .../app/controllers/user_controller.rb | 0 .../plugins/bad_plugin/lib/plugin_controller.rb | 0 actionpack/test/controller/mime_type_test.rb | 1 + actionpack/test/controller/routing_test.rb | 1811 +++++++++++--------- actionpack/test/controller/test_test.rb | 13 +- actionpack/test/controller/url_rewriter_test.rb | 17 - 15 files changed, 1946 insertions(+), 1672 deletions(-) delete mode 100644 actionpack/lib/action_controller/code_generation.rb create mode 100644 actionpack/test/controller/controller_fixtures/app/controllers/admin/user_controller.rb create mode 100644 actionpack/test/controller/controller_fixtures/app/controllers/user_controller.rb create mode 100644 actionpack/test/controller/controller_fixtures/vendor/plugins/bad_plugin/lib/plugin_controller.rb (limited to 'actionpack') diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 47d5f8fb8c..2af08a5ae2 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,11 @@ *SVN* +* Routing rewrite. Simpler, faster, easier to understand. The published API for config/routes.rb is unchanged, but nearly everything else is different, so expect breakage in plugins and libs that try to fiddle with routes. [Nicholas Seckar, Jamis Buck] + + map.connect '/foo/:id', :controller => '...', :action => '...' + map.connect '/foo/:id.:format', :controller => '...', :action => '...' + map.connect '/foo/:id', ..., :conditions => { :method => :get } + * Cope with missing content type and length headers. Parse parameters from multipart and urlencoded request bodies only. [Jeremy Kemper] * Accept multipart PUT parameters. #5235 [guy.naor@famundo.com] @@ -37,7 +43,6 @@ All this relies on the fact that you have a route that includes .:format. - * Expanded :method option in FormTagHelper#form_tag, FormHelper#form_for, PrototypeHelper#remote_form_for, PrototypeHelper#remote_form_tag, and PrototypeHelper#link_to_remote to allow for verbs other than GET and POST by automatically creating a hidden form field named _method, which will simulate the other verbs over post [DHH] * Added :method option to UrlHelper#link_to, which allows for using other verbs than GET for the link. This replaces the :post option, which is now deprecated. Example: link_to "Destroy", person_url(:id => person), :method => :delete [DHH] diff --git a/actionpack/lib/action_controller/assertions.rb b/actionpack/lib/action_controller/assertions.rb index 4e3443c456..a89f515af6 100644 --- a/actionpack/lib/action_controller/assertions.rb +++ b/actionpack/lib/action_controller/assertions.rb @@ -189,7 +189,7 @@ module Test #:nodoc: # Load routes.rb if it hasn't been loaded. ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? - generated_path, extra_keys = ActionController::Routing::Routes.generate(options, extras) + generated_path, extra_keys = ActionController::Routing::Routes.generate_extras(options, extras) found_extras = options.reject {|k, v| ! extra_keys.include? k} msg = build_message(message, "found extras , not ", found_extras, extras) @@ -365,7 +365,8 @@ module Test #:nodoc: request = ActionController::TestRequest.new({}, {}, nil) request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method request.path = path - ActionController::Routing::Routes.recognize!(request) + + ActionController::Routing::Routes.recognize(request) request end diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 098b7265de..0902e70082 100755 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -2,7 +2,6 @@ require 'action_controller/mime_type' require 'action_controller/request' require 'action_controller/response' require 'action_controller/routing' -require 'action_controller/code_generation' require 'action_controller/url_rewriter' require 'drb' require 'set' diff --git a/actionpack/lib/action_controller/code_generation.rb b/actionpack/lib/action_controller/code_generation.rb deleted file mode 100644 index 312dff6be0..0000000000 --- a/actionpack/lib/action_controller/code_generation.rb +++ /dev/null @@ -1,243 +0,0 @@ -module ActionController - module CodeGeneration #:nodoc: - class GenerationError < StandardError #:nodoc: - end - - class Source #:nodoc: - 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 #:nodoc: - 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) || value.is_a?(Symbol) - copy.send("#{sym}=", value) - end - return copy - end - end - - class RecognitionGenerator < CodeGenerator #:nodoc: - Attributes = [:after, :before, :current, :results, :constants, :depth, :move_ahead, :finish_statement, :path_name, :base_segment_name, :base_index_name] - 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} - @path_name = :path - @base_segment_name = :segment - @base_index_name = :index - 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.to_s - end - - def segment_name() "#{base_segment_name}#{depth}".to_sym end - def index_name - move_ahead, @move_ahead = @move_ahead, nil - move_ahead ? "#{base_index_name} += #{move_ahead}" : base_index_name - 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 #:nodoc: - Attributes = [:after, :before, :current, :segments, :subpath_at] - attr_accessor(*Attributes) - FieldsToDuplicate = CodeGenerator::FieldsToDuplicate + Attributes - - def initialize(*args) - super(*args) - @after, @before = [], [] - @current = nil - @segments = [] - @subpath_at = nil - 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 start_subpath! - @subpath_at ||= segments.length - end - - def finish - segments[subpath_at..-1] = [segments[subpath_at..-1].join(";")] if subpath_at - 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/routing.rb b/actionpack/lib/action_controller/routing.rb index 67339a6f21..e27520e2e8 100644 --- a/actionpack/lib/action_controller/routing.rb +++ b/actionpack/lib/action_controller/routing.rb @@ -1,739 +1,1038 @@ +require 'cgi' +require 'pathname' + +class Object + def to_param + to_s + end +end + +class TrueClass + def to_param + self + end +end + +class FalseClass + def to_param + self + end +end + +class NilClass + def to_param + self + end +end + +class Regexp + def number_of_captures + Regexp.new("|#{source}").match('').captures.length + end + + class << self + def optionalize(pattern) + case unoptionalize(pattern) + when /\A(.|\(.*\))\Z/ then "#{pattern}?" + else "(?:#{pattern})?" + end + end + + def unoptionalize(pattern) + [/\A\(\?:(.*)\)\?\Z/, /\A(.|\(.*\))\?\Z/].each do |regexp| + return $1 if regexp =~ pattern + end + return pattern + end + end +end + module ActionController - module Routing #:nodoc: + module Routing + SEPARATORS = %w( / ; . , ? ) + 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 + def with_controllers(names) + use_controllers! names + yield + ensure + use_controllers! nil + end + + def possible_controllers + unless @possible_controllers + @possible_controllers = [] + + paths = $LOAD_PATH.select { |path| File.directory? path } + paths.collect! { |path| Pathname.new(path).realpath.to_s } + paths = paths.sort_by { |path| - path.length } + + seen_paths = Hash.new {|h, k| h[k] = true; false} + paths.each do |load_path| + Dir["#{load_path}/**/*_controller.rb"].collect do |path| + next if seen_paths[path] + + controller_name = path[(load_path.length + 1)..-1] + controller_name.gsub!(/_controller\.rb\Z/, '') + @possible_controllers << controller_name + end + end + end + @possible_controllers end - def extract_parameter_value(parameter) #:nodoc: - CGI.escape((parameter.respond_to?(:to_param) ? parameter.to_param : parameter).to_s) + def use_controllers!(controller_names) + @possible_controllers = controller_names 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 + end + end + + class Route + attr_accessor :segments, :requirements, :conditions + + def initialize + @segments = [] + @requirements = {} + @conditions = {} end + + # Write and compile a +generate+ method for this Route. + def write_generation + # Build the main body of the generation + body = "not_expired = true\n#{generation_extraction}\n#{generation_structure}" + + # If we have conditions that must be tested first, nest the body inside an if + body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements + args = "options, hash, expire_on = {}" + + # Nest the body inside of a def block, and then compile it. + method_decl = "def generate_raw(#{args})\path = begin\n#{body}\nend\n[path, hash]\nend" +# puts "\n======================" +# puts +# p self +# puts +# puts method_decl +# puts + instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" + + method_decl = "def generate(#{args})\nappend_query_string(*generate_raw(options, hash, expire_on))\nend" + instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" - def treat_hash(hash, keys_to_delete = []) - k = v = nil - hash.each do |k, v| - if v then hash[k] = (v.respond_to? :to_param) ? v.to_param.to_s : v.to_s + method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, hash.keys.map(&:to_sym) - significant_keys]\nend" + instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" + end + + # Build several lines of code that extract values from the options hash. If any + # of the values are missing or rejected then a return will be executed. + def generation_extraction + segments.collect do |segment| + segment.extraction_code + end.compact * "\n" + end + + # Produce a condition expression that will check the requirements of this route + # upon generation. + def generation_requirements + requirement_conditions = requirements.collect do |key, req| + if req.is_a? Regexp + value_regexp = Regexp.new "\\A#{req.source}\\Z" + "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]" else - hash.delete k - keys_to_delete << k + "hash[:#{key}] == #{req.inspect}" end end - hash - end - - 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 Array then - conds = condition.collect do |condition| - cond = test_condition(expression, condition) - (cond[0, 1] == '(' && cond[-1, 1] == ')') ? cond : "(#{cond})" - end - "(#{conds.join(' || ')})" - when true then expression - when nil then "! #{expression}" - else - raise ArgumentError, "Valid criteria are strings, regular expressions, true, or nil" - end + requirement_conditions * ' && ' unless requirement_conditions.empty? + end + def generation_structure + segments.last.string_structure segments[0..-2] end - end - - class Component #:nodoc: - def dynamic?() false end - def optional?() false end - - def key() nil end - def self.new(string, *args) - return super(string, *args) unless self == Component - case string - when /.*;.*/ then SubpathComponent.new(string.split(/;/), *args) - 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 - - class SubpathComponent < Component #:nodoc: - attr_reader :parts + # Write and compile a +recognize+ method for this Route. + def write_recognition + # Create an if structure to extract the params from a match if it occurs. + body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams" + body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend" + + # Build the method declaration and compile it + method_decl = "def recognize(path, env={})\n#{body}\nend" +# puts "\n======================" +# puts +# p self +# puts +# puts method_decl +# puts + instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" + method_decl + end - def initialize(parts, *args) - @parts = parts.map { |part| Component.new(part, *args) } + # Plugins may override this method to add other conditions, like checks on + # host, subdomain, and so forth. Note that changes here only affect route + # recognition, not generation. + def recognition_conditions + result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"] + result << "conditions[:method] === env[:method]" if conditions[:method] + result end - def write_recognition(g) - raise RoutingError, "Subpath components must occur last" unless g.after.empty? - g.if("#{g.next_segment(true)} && #{g.next_segment}.include?(';')") do |gp| - gp.line "subindex, subpath = 0, #{gp.next_segment}.split(/;/)" - tweak_recognizer(gp).go - gp.move_forward { |gpp| gpp.continue } + # Build the regular expression pattern that will match this route. + def recognition_pattern(wrap = true) + pattern = '' + segments.reverse_each do |segment| + pattern = segment.build_pattern pattern end + wrap ? ("\\A" + pattern + "\\Z") : pattern end - - def write_generation(g) - raise RoutingError, "Subpath components must occur last" unless g.after.empty? - tweak_generator(g).go + + # Write the code to extract the parameters from a matched route. + def recognition_extraction + next_capture = 1 + extraction = segments.collect do |segment| + x = segment.match_extraction next_capture + next_capture += Regexp.new(segment.regexp_chunk).number_of_captures + x + end + extraction.compact end - - def key - parts.map { |p| p.key } + + # Write the real generation implementation and then resend the message. + def generate(options, hash, expire_on = {}) + write_generation + generate options, hash, expire_on end - private + def generate_extras(options, hash, expire_on = {}) + write_generation + generate_extras options, hash, expire_on + end - def tweak_recognizer(g) - gg = g.dup + # Generate the query string with any extra keys in the hash and append + # it to the given path, returning the new path. + def append_query_string(path, hash) + return nil unless path + query = hash.keys.map(&:to_sym) - significant_keys + "#{path}#{build_query_string(hash, query)}" + end - gg.path_name = :subpath - gg.base_segment_name = :subsegment - gg.base_index_name = :subindex - gg.depth = 0 + # Build a query string from the keys of the given hash. If +only_keys+ + # is given (as an array), only the keys indicated will be used to build + # the query string. The query string will correctly build array parameter + # values. + def build_query_string(hash, only_keys=nil) + elements = [] - gg.before, gg.current, gg.after = [], parts.first, (parts[1..-1] || []) + only_keys ||= hash.keys + + only_keys.each do |key| + value = hash[key] + key = CGI.escape key.to_s + if value.class == Array + key << '[]' + else + value = [ value ] + end + value.each { |val| elements << "#{key}=#{CGI.escape(val.to_param.to_s)}" } + end + + query_string = "?#{elements.join("&")}" unless elements.empty? + query_string || "" + end + + # Write the real recognition implementation and then resend the message. + def recognize(path, environment={}) + write_recognition + recognize path, environment + end + + # A route's parameter shell contains parameter values that are not in the + # route's path, but should be placed in the recognized hash. + # + # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route: + # + # map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/ + # + def parameter_shell + @parameter_shell ||= returning({}) do |shell| + requirements.each do |key, requirement| + shell[key] = requirement unless requirement.is_a? Regexp + end + end + end + + # Return an array containing all the keys that are used in this route. This + # includes keys that appear inside the path, and keys that have requirements + # placed upon them. + def significant_keys + @significant_keys ||= returning [] do |sk| + segments.each { |segment| sk << segment.key if segment.respond_to? :key } + sk.concat requirements.keys + sk.uniq! + end + end - gg + # Return a hash of key/value pairs representing the keys in the route that + # have defaults, or which are specified by non-regexp requirements. + def defaults + @defaults ||= returning({}) do |hash| + segments.each do |segment| + next unless segment.respond_to? :default + hash[segment.key] = segment.default unless segment.default.nil? + end + requirements.each do |key,req| + next if Regexp === req || req.nil? + hash[key] = req + end end + end + + def matches_controller_and_action?(controller, action) + unless @matching_prepared + @controller_requirement = requirement_for(:controller) + @action_requirement = requirement_for(:action) + @matching_prepared = true + end + + (@controller_requirement.nil? || @controller_requirement === controller) && + (@action_requirement.nil? || @action_requirement === action) + end - def tweak_generator(g) - gg = g.dup - gg.before, gg.current, gg.after = [], parts.first, (parts[1..-1] || []) - gg.start_subpath! - gg + def to_s + @to_s ||= segments.inject("") { |str,s| str << s.to_s } + end + + protected + + def requirement_for(key) + return requirements[key] if requirements.key? key + segments.each do |segment| + return segment.regexp if segment.respond_to?(:key) && segment.key == key end + nil + end + end - class StaticComponent < Component #:nodoc: - attr_reader :value - - def initialize(value) - @value = value + class Segment + attr_accessor :is_optional + alias_method :optional?, :is_optional + + def initialize + self.is_optional = false end - def write_recognition(g) - g.if_next_matches(value) do |gp| - gp.move_forward {|gpp| gpp.continue} + def extraction_code + nil + end + + # Continue generating string for the prior segments. + def continue_string_structure(prior_segments) + if prior_segments.empty? + interpolation_statement(prior_segments) + else + new_priors = prior_segments[0..-2] + prior_segments.last.string_structure(new_priors) end end - - def write_generation(g) - g.add_segment(value) {|gp| gp.continue } + + # Return a string interpolation statement for this segment and those before it. + def interpolation_statement(prior_segments) + chunks = prior_segments.collect { |s| s.interpolation_chunk } + chunks << interpolation_chunk + "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}" + end + + def string_structure(prior_segments) + optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments) + end + + # Return an if condition that is true if all the prior segments can be generated. + # If there are no optional segments before this one, then nil is returned. + def all_optionals_available_condition(prior_segments) + optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact + optional_locals.empty? ? nil : " if #{optional_locals * ' && '}" + end + + # Recognition + + def match_extraction(next_capture) + nil + end + + # Warning + + # Returns true if this segment is optional? because of a default. If so, then + # no warning will be emitted regarding this segment. + def optionality_implied? + false end + end - class DynamicComponent < Component #:nodoc: - attr_reader :key, :default - attr_accessor :condition + class StaticSegment < Segment + attr_accessor :value, :raw + alias_method :raw?, :raw - def dynamic?() true end - def optional?() @optional end - - def default=(default) - @optional = true - @default = default + def initialize(value = nil) + super() + self.value = value end - - def initialize(key, options = {}) - @key = key.to_sym - @optional = false - default, @condition = options[:default], options[:condition] - self.default = default if options.key?(:default) + + def interpolation_chunk + raw? ? value : CGI.escape(value) end - - def default_check(g) - presence = "#{g.hash_value(key, !! default)}" - if default - "!(#{presence} && #{g.hash_value(key, false)} != #{default.to_s.inspect})" + + def regexp_chunk + chunk = Regexp.escape value + optional? ? Regexp.optionalize(chunk) : chunk + end + + def build_pattern(pattern) + escaped = Regexp.escape(value) + if optional? && ! pattern.empty? + "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})" + elsif optional? + Regexp.optionalize escaped else - "! #{presence}" + escaped + pattern end end - def write_generation(g) - wrote_dropout = write_dropout_generation(g) - write_continue_generation(g, wrote_dropout) + def to_s + value end + + 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) - - check += gp.after.map {|c| c.default_check gp} - gp.if(check.join(' && ')) { gp.finish } # If this condition is met, we stop here - true + class DividerSegment < StaticSegment + + def initialize(value = nil) + super(value) + self.raw = true + self.is_optional = 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 + + def optionality_implied? + true end + + end - def add_segments_to(g) - g.add_segment(%(\#{CGI.escape(#{g.hash_value(key, true, default)})})) {|gp| yield gp} + class DynamicSegment < Segment + attr_accessor :key, :default, :regexp + + def initialize(key = nil, options = {}) + super() + self.key = key + self.default = options[:default] if options.key? :default + self.is_optional = true if options[:optional] || options.key?(:default) 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} + def to_s + ":#{key}" end - def write_recognition(g) - test_type = nil - recognition_check(g) do |gp, test_type| - assign_result(gp) {|gpp| gpp.continue} + # The local variable name that the value of this segment will be extracted to. + def local_name + "#{key}_value" + end + + def extract_value + "#{local_name} = hash[:#{key}] #{"|| #{default.inspect}" if default}" + end + def value_check + if default # Then we know it won't be nil + "#{value_regexp.inspect} =~ #{local_name}" if regexp + elsif optional? + # If we have a regexp check that the value is not given, or that it matches. + # If we have no regexp, return nil since we do not require a condition. + "#{local_name}.nil? || #{value_regexp.inspect} =~ #{local_name}" if regexp + else # Then it must be present, and if we have a regexp, it must match too. + "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}" 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 + def expiry_statement + "not_expired, hash = false, options if not_expired && expire_on[:#{key}]" + end + + def extraction_code + s = extract_value + vc = value_check + s << "\nreturn [nil,nil] unless #{vc}" if vc + s << "\n#{expiry_statement}" + end + + def interpolation_chunk + "\#{CGI.escape(#{local_name}.to_s)}" + end + + def string_structure(prior_segments) + if optional? # We have a conditional to do... + # If we should not appear in the url, just write the code for the prior + # segments. This occurs if our value is the default value, or, if we are + # optional, if we have nil as our value. + "if #{local_name} == #{default.inspect}\n" + + continue_string_structure(prior_segments) + + "\nelse\n" + # Otherwise, write the code up to here + "#{interpolation_statement(prior_segments)}\nend" + else + interpolation_statement(prior_segments) end end - - def assign_result(g, with_default = false) - g.result key, "CGI.unescape(#{g.next_segment(true, with_default ? default : nil)})" - g.move_forward {|gp| yield gp} + + def value_regexp + Regexp.new "\\A#{regexp.source}\\Z" if regexp end - - def assign_default(g) - g.constant_result key, default unless default.nil? + def regexp_chunk + regexp ? regexp.source : "([^#{Routing::SEPARATORS.join}]+)" end + + def build_pattern(pattern) + chunk = regexp_chunk + chunk = "(#{chunk})" if Regexp.new(chunk).number_of_captures == 0 + pattern = "#{chunk}#{pattern}" + optional? ? Regexp.optionalize(pattern) : pattern + end + def match_extraction(next_capture) + hangon = (default ? "|| #{default.inspect}" : "if match[#{next_capture}]") + "params[:#{key}] = match[#{next_capture}] #{hangon}" + end + + def optionality_implied? + [:action, :id].include? key + 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' - if condition - gp << "controller_path = #{gp.path_name}[#{gp.index_name},segments_to_controller].join('/')" - gp.if(Routing.test_condition("controller_path", condition)) do |gpp| - gpp.move_forward('segments_to_controller') {|gppp| yield gppp, :constraint} - end - else - gp.move_forward('segments_to_controller') {|gpp| yield gpp, :constraint} - end - end + class ControllerSegment < DynamicSegment + def regexp_chunk + possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name } + "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))" end - def assign_result(g) - g.result key, 'controller_value' - yield g + # Don't CGI.escape the controller name, since it may have slashes in it, + # like admin/foo. + def interpolation_chunk + "\#{#{local_name}.to_s}" end - def assign_default(g) - ControllerComponent.assign_controller(g, default) + # Make sure controller names like Admin/Content are correctly normalized to + # admin/content + def extract_value + "#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default}).downcase" end - - class << self - def assign_controller(g, controller) - expr = "::#{controller.split('/').collect {|c| c.camelize}.join('::')}Controller" - g.result :controller, expr, true - end - def traverse_to_controller(segments, start_at = 0) - mod = ::Object - length = segments.length - index = start_at - mod_name = controller_name = segment = nil - - while index < length - return nil unless /^[A-Za-z][A-Za-z\d_]*$/ =~ (segment = segments[index]) - index += 1 - - mod_name = segment.camelize - controller_name = "#{mod_name}Controller" - - begin - # We use eval instead of const_get to avoid obtaining values from parent modules. - controller = eval("mod::#{controller_name}", nil, __FILE__, __LINE__) - expected_name = "#{mod.name}::#{controller_name}" - - # Detect the case when const_get returns an object from a parent namespace. - if controller.is_a?(Class) && controller.ancestors.include?(ActionController::Base) && (mod == Object || controller.name == expected_name) - return controller, (index - start_at) - end - rescue NameError => e - raise unless /^uninitialized constant .*#{controller_name}$/ =~ e.message - end - - begin - next_mod = eval("mod::#{mod_name}", nil, __FILE__, __LINE__) - # Check that we didn't get a module from a parent namespace - mod = (mod == Object || next_mod.name == "#{mod.name}::#{mod_name}") ? next_mod : nil - rescue NameError => e - raise unless /^uninitialized constant .*#{mod_name}$/ =~ e.message - end - - return nil unless mod - end - end + def match_extraction(next_capture) + hangon = (default ? "|| #{default.inspect}" : "if match[#{next_capture}]") + "params[:#{key}] = match[#{next_capture}].downcase #{hangon}" end end - class PathComponent < DynamicComponent #:nodoc: - def optional?() true end - def default() [] end - def condition() nil end + class PathSegment < DynamicSegment + EscapedSlash = CGI.escape("/") + def interpolation_chunk + "\#{CGI.escape(#{local_name}).gsub(#{EscapedSlash.inspect}, '/')}" + end - def default=(value) - raise RoutingError, "All path components have an implicit default of []" unless value == [] + def default + '' 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 } + + def default=(path) + raise RoutingError, "paths cannot have non-empty default values" unless path.blank? end - - def write_recognition(g) - raise RoutingError, "Path components must occur last" unless g.after.empty? - - start = g.index_name.to_s - start = "(#{start})" unless /^\w+$/ =~ start.to_s - - value_expr = "#{g.path_name}[#{start}..-1] || []" - g.result key, "ActionController::Routing::PathComponent::Result.new_escaped(#{value_expr})" - g.finish(false) + + def match_extraction(next_capture) + "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}" end - + + def regexp_chunk + regexp || "(.*)" + end + class Result < ::Array #:nodoc: - def to_s() join '/' end + def to_s() join '/' end def self.new_escaped(strings) new strings.collect {|str| CGI.unescape str} - end - end + end + end end - class Route #:nodoc: - attr_accessor :components, :known - attr_reader :path, :options, :keys, :defaults + class RouteBuilder + attr_accessor :separators, :optional_separators - def initialize(path, options = {}) - @path, @options = path, options - - initialize_components path - defaults, conditions = initialize_hashes options.dup - @defaults = defaults.dup - @request_method = conditions.delete(:method) - configure_components(defaults, conditions) - add_default_requirements - initialize_keys + def initialize + self.separators = Routing::SEPARATORS + self.optional_separators = %w( / ) end - def inspect - "<#{self.class} #{path.inspect}, #{options.inspect[1..-1]}>" + def separator_pattern(inverted = false) + "[#{'^' if inverted}#{Regexp.escape(separators.join)}]" end - def write_generation(generator = CodeGeneration::GenerationGenerator.new) - generator.before, generator.current, generator.after = [], components.first, (components[1..-1] || []) + def interval_regexp + Regexp.new "(.*?)(#{separators.source}|$)" + end + + # Accepts a "route path" (a string defining a route), and returns the array + # of segments that corresponds to it. Note that the segment array is only + # partially initialized--the defaults and requirements, for instance, need + # to be set separately, via the #assign_route_options method, and the + # #optional? method for each segment will not be reliable until after + # #assign_route_options is called, as well. + def segments_for_route_path(path) + rest, segments = path, [] + + until rest.empty? + segment, rest = segment_for rest + segments << segment + end + segments + end - if known.empty? then generator.go - else - # Alter the conditions to allow :action => 'index' to also catch :action => nil - altered_known = known.collect do |k, v| - if k == :action && v== 'index' then [k, [nil, 'index']] - else [k, v] + # A factory method that returns a new segment instance appropriate for the + # format of the given string. + def segment_for(string) + segment = case string + when /\A:(\w+)/ + key = $1.to_sym + case key + when :action then DynamicSegment.new(key, :default => 'index') + when :id then DynamicSegment.new(key, :optional => true) + when :controller then ControllerSegment.new(key) + else DynamicSegment.new key + end + when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true) + when /\A\?(.*?)\?/ + returning segment = StaticSegment.new($1) do + segment.is_optional = true + end + when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1) + when Regexp.new(separator_pattern) then + returning segment = DividerSegment.new($&) do + segment.is_optional = (optional_separators.include? $&) end - end - generator.if(generator.check_conditions(altered_known)) {|gp| gp.go } end - - generator + [segment, $~.post_match] 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] || []) + # Split the given hash of options into requirement and default hashes. The + # segments are passed alongside in order to distinguish between default values + # and requirements. + def divide_route_options(segments, options) + requirements = options.delete(:requirements) || {} + defaults = options.delete(:defaults) || {} + conditions = options.delete(:conditions) || {} - known.each do |key, value| - if key == :controller then ControllerComponent.assign_controller(g, value) - else g.constant_result(key, value) - end - end - - if @request_method - g.if("@request.method == :#{@request_method}") { |gp| gp.go } - else - g.go + path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact + options.each do |key, value| + hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements + hash[key] = value end - generator - end - - def initialize_keys - @keys = (components.collect {|c| c.key} + known.keys).flatten.compact - @keys.freeze + [defaults, requirements, conditions] end - def extra_keys(options) - options.keys - @keys - end + # Takes a hash of defaults and a hash of requirements, and assigns them to + # the segments. Any unused requirements (which do not correspond to a segment) + # are returned as a hash. + def assign_route_options(segments, defaults, requirements) + route_requirements = {} # Requirements that do not belong to a segment - def matches_controller?(controller) - if known[:controller] then known[:controller] == controller - else - c = components.find {|c| c.key == :controller} - return false unless c - return c.condition.nil? || eval(Routing.test_condition('controller', c.condition)) - end - end - - protected - def initialize_components(path) - path = path.split('/') if path.is_a? String - path.shift if path.first.blank? - self.components = path.collect {|str| Component.new str} + segment_named = Proc.new do |key| + segments.detect { |segment| segment.key == key if segment.respond_to?(:key) } end - def initialize_hashes(options) - path_keys = components.collect {|c| c.key }.flatten.compact - self.known = {} - defaults = options.delete(:defaults) || {} - conditions = options.delete(:require) || {} - conditions.update(options.delete(:requirements) || {}) - - options.each do |k, v| - if path_keys.include?(k) then (v.is_a?(Regexp) ? conditions : defaults)[k] = v - else known[k] = v - end + requirements.each do |key, requirement| + segment = segment_named[key] + if segment + raise TypeError, "#{key}: requirements on a path segment must be regular expressions" unless requirement.is_a?(Regexp) + segment.regexp = requirement + else + route_requirements[key] = requirement end - [defaults, conditions] end - def configure_components(defaults, conditions) - all_components = components.map { |c| SubpathComponent === c ? c.parts : c }.flatten - all_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 - - component.condition = conditions[component.key] if conditions.key?(component.key) - end - end - - def add_default_requirements - component_keys = components.collect {|c| c.key}.flatten - known[:action] ||= 'index' unless component_keys.include? :action + defaults.each do |key, default| + segment = segment_named[key] + raise ArgumentError, "#{key}: No matching segment exists; cannot assign default" unless segment + segment.is_optional = true + segment.default = default.to_param if default end - end - class RouteSet #:nodoc: - attr_reader :routes, :categories, :controller_to_selector - def initialize - @routes = [] - @generation_methods = Hash.new(:generate_default_path) - end - - 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 - use_recall = true - - controller = options[:controller] - options[:action] ||= 'index' if controller - recall_controller = recall[:controller] - if (recall_controller && recall_controller.include?(?/)) || (controller && controller.include?(?/)) - recall = {} if controller && controller[0] == ?/ - options[:controller] = Routing.controller_relative_to(controller, recall_controller) - end - options = recall.dup if options.empty? # XXX move to url_rewriter? - - keys_to_delete = [] - Routing.treat_hash(options, keys_to_delete) - - merged = recall.merge(options) - keys_to_delete.each {|key| merged.delete key} - expire_on = Routing.expiry_hash(options, recall) - - generate_path(merged, options, expire_on) - end - - def generate_path(merged, options, expire_on) - send @generation_methods[merged[:controller]], merged, options, expire_on - end - def generate_default_path(*args) - write_generation - generate_default_path(*args) + ensure_required_segments(segments) + route_requirements end - def write_generation - method_sources = [] - @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 - method_sources << code - - filename = "generated_code/routing/generation_for_controller_#{controller}.rb" - eval(code, nil, filename) - - @generation_methods[controller.to_s] = method_name - @generation_methods[controller.to_sym] = method_name + # Makes sure that there are no optional segments that precede a required + # segment. If any are found that precede a required segment, they are + # made required. + def ensure_required_segments(segments) + allow_optional = true + segments.reverse_each do |segment| + allow_optional &&= segment.optional? + if !allow_optional && segment.optional? + unless segment.optionality_implied? + warn "Route segment \"#{segment.to_s}\" cannot be optional because it precedes a required segment. This segment will be required." + end + segment.is_optional = false + elsif allow_optional & segment.respond_to?(:default) && segment.default + # if a segment has a default, then it is optional + segment.is_optional = true + end end - - code = generation_code_for('routes', 'generate_default_path').to_s - eval(code, nil, 'generated_code/routing/generation.rb') - - return (method_sources << code) end - def recognize(request) - @request = request + # Construct and return a route with the given path and options. + def build(path, options) + # Wrap the path with slashes + path = "/#{path}" unless path[0] == ?/ + path = "#{path}/" unless path[-1] == ?/ + + segments = segments_for_route_path(path) + defaults, requirements, conditions = divide_route_options(segments, options) + requirements = assign_route_options(segments, defaults, requirements) + + route = Route.new + route.segments = segments + route.requirements = requirements + route.conditions = conditions - string_path = @request.path - string_path.chomp! if string_path[0] == ?/ - path = string_path.split '/' - path.shift - - hash = recognize_path(path) - return recognition_failed(@request) unless hash && hash['controller'] - - controller = hash['controller'] - hash['controller'] = controller.controller_path - @request.path_parameters = hash - controller.new + if !route.significant_keys.include?(:action) && !route.requirements[:action] + route.requirements[:action] = "index" + route.significant_keys << :action + end + + route end - alias :recognize! :recognize + end + + class RouteSet - def recognition_failed(request) - raise ActionController::RoutingError, "Recognition failed for #{request.path.inspect}" + # Mapper instances are used to build routes. The object passed to the draw + # block in config/routes.rb is a Mapper instance. + # + # Mapper instances have relatively few instance methods, in order to avoid + # clashes with named routes. + class Mapper + def initialize(set) + @set = set + end + + # Create an unnamed route with the provided +path+ and +options+. See + # SomeHelpfulUrl for an introduction to routes. + def connect(path, options = {}) + @set.add_route(path, options) + end + + def method_missing(route_name, *args, &proc) + super unless args.length >= 1 && proc.nil? + @set.add_named_route(route_name, *args) + end end - 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) + # A NamedRouteCollection instance is a collection of named routes, and also + # maintains an anonymous module that can be used to install helpers for the + # named routes. + class NamedRouteCollection + include Enumerable + + attr_reader :routes, :helpers + + def initialize + clear! + end + + def clear! + @routes = {} + @helpers = [] + @module = Module.new + end + + def add(name, route) + routes[name.to_sym] = route + define_hash_access_method(name, route) + define_url_helper_method(name, route) + end + + def get(name) + routes[name.to_sym] + end + + alias []= add + alias [] get + alias clear clear! + + def each + routes.each { |name, route| yield name, route } + self + end + + def names + routes.keys + end + + def length + routes.length + end + + def install(dest = ActionController::Base) + dest.send :include, @module + if dest.respond_to? :helper_method + helpers.each { |name| dest.send :helper_method, name } end end - eval g.to_s, nil, 'generated/routing/recognition.rb' - return 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) + private + + def url_helper_name(name) + :"#{name}_url" + end + + def hash_access_name(name) + :"hash_for_#{name}_url" + end + + def define_hash_access_method(name, route) + method_name = hash_access_name(name) + + @module.send(:define_method, method_name) do |*args| + hash = route.defaults.merge(:use_route => name) + args.first ? hash.merge(args.first) : hash + end + + @module.send(:protected, method_name) + helpers << method_name + end + + def define_url_helper_method(name, route) + hash_access_method = hash_access_name(name) + method_name = url_helper_name(name) + + @module.send(:define_method, method_name) do |*args| + opts = if args.empty? || Hash === args.first + args.first || {} else - g.if "(new_unused_count = new_unused_keys.length) < unused_count" do |gp| - gp << "hash = merged; not_expired = true" - route.write_generation(gp) + # allow ordered parameters to be associated with corresponding + # dynamic segments, so you can do + # + # foo_url(bar, baz, bang) + # + # instead of + # + # foo_url(:bar => bar, :baz => baz, :bang => bang) + route.segments.inject({}) do |opts, seg| + next opts unless seg.respond_to?(:key) && seg.key + opts[seg.key] = args.shift + break opts if args.empty? + opts end end + + url_for(send(hash_access_method, opts)) 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 + + @module.send(:protected, method_name) + helpers << method_name end - known - end.uniq + end + + attr_accessor :routes, :named_routes + + def initialize + self.routes = [] + self.named_routes = NamedRouteCollection.new end - def reload - NamedRoutes.clear - - if defined?(RAILS_ROOT) then load(File.join(RAILS_ROOT, 'config', 'routes.rb')) - else connect(':controller/:action/:id', :action => 'index', :id => nil) - end + # Subclasses and plugins may override this method to specify a different + # RouteBuilder instance, so that other route DSL's can be created. + def builder + @builder ||= RouteBuilder.new + end - NamedRoutes.install + def draw + clear! + yield Mapper.new(self) + named_routes.install + end + + def clear! + routes.clear + named_routes.clear + @combined_regexp = nil + @routes_by_controller = nil end - def connect(*args) - new_route = Route.new(*args) - @routes << new_route - return new_route + def empty? + routes.empty? + end + + def load! + clear! + load_routes! + named_routes.install end - def draw - old_routes = @routes - @routes = [] - - begin yield self - rescue - @routes = old_routes - raise + alias reload load! + + def load_routes! + if defined?(RAILS_ROOT) && defined?(::ActionController::Routing::Routes) && self == ::ActionController::Routing::Routes + load File.join("#{RAILS_ROOT}/config/routes.rb") + else + add_route ":controller/:action/:id" end - write_generation - write_recognition end - - def empty?() @routes.empty? end - - def each(&block) @routes.each(&block) end - - # Defines a new named route with the provided name and arguments. - # This method need only be used when you wish to use a name that a RouteSet instance - # method exists for, such as categories. - # - # For example, map.categories '/categories', :controller => 'categories' will not work - # due to RouteSet#categories. - def named_route(name, path, hash = {}) - route = connect(path, hash) - NamedRoutes.name_route(route, name) + + def add_route(path, options = {}) + route = builder.build(path, options) + routes << route route end - - def method_missing(name, *args) - (1..2).include?(args.length) ? named_route(name, *args) : super(name, *args) + + def add_named_route(name, path, options = {}) + named_routes[name] = add_route(path, options) end + + def options_as_params(options) + # If an explicit :controller was given, always make :action explicit + # too, so that action expiry works as expected for things like + # + # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'}) + # + # (the above is from the unit tests). In the above case, because the + # controller was explicitly given, but no action, the action is implied to + # be "index", not the recalled action of "show". + # + # great fun, eh? - def extra_keys(options, recall = {}) - generate(options.dup, recall).last + options_as_params = options[:controller] ? { :action => "index" } : {} + options.each do |k, value| + options_as_params[k] = value.to_param + end + options_as_params end - end - - module NamedRoutes #:nodoc: - Helpers = [] - class << self - def clear() Helpers.clear end - def hash_access_name(name) - "hash_for_#{name}_url" + def build_expiry(options, recall) + recall.inject({}) do |expiry, (key, recalled_value)| + expiry[key] = (options.key?(key) && options[key] != recalled_value) + expiry end + end - def url_helper_name(name) - "#{name}_url" + # Generate the path indicated by the arguments, and return an array of + # the keys that were not used to generate it. + def extra_keys(options, recall={}) + generate_extras(options, recall).last + end + + def generate_extras(options, recall={}) + generate(options, recall, :generate_extras) + end + + def generate(options, recall = {}, method=:generate) + if options[:use_route] + options = options.dup + named_route = named_routes[options.delete(:use_route)] + options = named_route.parameter_shell.merge(options) end - - def known_hash_for_route(route) - hash = route.known.symbolize_keys - route.defaults.each do |key, value| - hash[key.to_sym] ||= value if value - end - hash[:controller] = "/#{hash[:controller]}" - - hash + + options = options_as_params(options) + expire_on = build_expiry(options, recall) + + # if the controller has changed, make sure it changes relative to the + # current controller module, if any. In other words, if we're currently + # on admin/get, and the new controller is 'set', the new controller + # should really be admin/set. + if expire_on[:controller] && options[:controller] && options[:controller][0] != ?/ + parts = recall[:controller].split('/')[0..-2] + [options[:controller]] + options[:controller] = parts.join('/') end - - def define_hash_access_method(route, name) - hash = known_hash_for_route(route) - define_method(hash_access_name(name)) do |*args| - args.first ? hash.merge(args.first) : hash + + # drop the leading '/' on the controller name + options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/ + merged = recall.merge(options) + + if named_route + return named_route.generate(options, merged, expire_on) + else + merged[:action] ||= 'index' + options[:action] ||= 'index' + + controller = merged[:controller] + action = merged[:action] + + raise "Need controller and action!" unless controller && action + routes = routes_by_controller[controller][action][merged.keys.sort_by { |x| x.object_id }] + + routes.each do |route| + results = route.send(method, options, merged, expire_on) + return results if results end end - - def name_route(route, name) - define_hash_access_method(route, name) - - module_eval(%{def #{url_helper_name name}(options = {}) - url_for(#{hash_access_name(name)}.merge(options.is_a?(Hash) ? options : { :id => options })) - end}, "generated/routing/named_routes/#{name}.rb") - - protected url_helper_name(name), hash_access_name(name) - - Helpers << url_helper_name(name).to_sym - Helpers << hash_access_name(name).to_sym - 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 + raise RoutingError, "No route matches #{options.inspect}" + end + + def recognize(request) + params = recognize_path(request.path, extract_request_environment(request)) + request.path_parameters = params.with_indifferent_access + "#{params[:controller].camelize}Controller".constantize + end + + def recognize_path(path, environment={}) + routes.each do |route| + result = route.recognize(path, environment) and return result + end + raise RoutingError, "no route found to match #{path.inspect} with #{environment.inspect}" + end + + def routes_by_controller + @routes_by_controller ||= Hash.new do |controller_hash, controller| + controller_hash[controller] = Hash.new do |action_hash, action| + action_hash[action] = Hash.new do |key_hash, keys| + key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys) end end end end + + def routes_for(options, merged, expire_on) + raise "Need controller and action!" unless controller && action + controller = merged[:controller] + merged = options if expire_on[:controller] + action = merged[:action] || 'index' + + routes_by_controller[controller][action][merged.keys] + end + + def routes_for_controller_and_action(controller, action) + selected = routes.select do |route| + route.matches_controller_and_action? controller, action + end + (selected.length == routes.length) ? routes : selected + end + + def routes_for_controller_and_action_and_keys(controller, action, keys) + selected = routes.select do |route| + route.matches_controller_and_action? controller, action + end + selected.sort_by do |route| + (keys - route.significant_keys).length + end + end + + # Subclasses and plugins may override this method to extract further attributes + # from the request, for use by route conditions and such. + def extract_request_environment(request) + { :method => request.method } + end end Routes = RouteSet.new end -end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/test_process.rb b/actionpack/lib/action_controller/test_process.rb index 4d79880278..3fb6713f7b 100644 --- a/actionpack/lib/action_controller/test_process.rb +++ b/actionpack/lib/action_controller/test_process.rb @@ -106,7 +106,7 @@ module ActionController #:nodoc: if value.is_a? Fixnum value = value.to_s elsif value.is_a? Array - value = ActionController::Routing::PathComponent::Result.new(value) + value = ActionController::Routing::PathSegment::Result.new(value) end if extra_keys.include?(key.to_sym) @@ -433,7 +433,7 @@ module ActionController #:nodoc: end def method_missing(selector, *args) - return @controller.send(selector, *args) if ActionController::Routing::NamedRoutes::Helpers.include?(selector) + return @controller.send(selector, *args) if ActionController::Routing::Routes.named_routes.helpers.include?(selector) return super end diff --git a/actionpack/lib/action_controller/url_rewriter.rb b/actionpack/lib/action_controller/url_rewriter.rb index bc82fda5db..872bfcc122 100644 --- a/actionpack/lib/action_controller/url_rewriter.rb +++ b/actionpack/lib/action_controller/url_rewriter.rb @@ -41,34 +41,9 @@ module ActionController options.update(overwrite) end RESERVED_OPTIONS.each {|k| options.delete k} - path, extra_keys = Routing::Routes.generate(options.dup, @request) # Warning: Routes will mutate and violate the options hash - path << build_query_string(options, extra_keys) unless extra_keys.empty? - - path - end - - # Returns a query string with escaped keys and values from the passed hash. If the passed hash contains an "id" it'll - # be added as a path element instead of a regular parameter pair. - def build_query_string(hash, only_keys = nil) - elements = [] - query_string = "" - - only_keys ||= hash.keys - - only_keys.each do |key| - value = hash[key] - key = CGI.escape key.to_s - if value.class == Array - key << '[]' - else - value = [ value ] - end - value.each { |val| elements << "#{key}=#{Routing.extract_parameter_value(val)}" } - end - - query_string << ("?" + elements.join("&")) unless elements.empty? - query_string + # Generates the query string, too + Routing::Routes.generate(options, @request.parameters) end end end diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index f55fb6cbf8..bd9c8c1ff8 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -227,10 +227,12 @@ class ActionPackAssertionsControllerTest < Test::Unit::TestCase # test the redirection to a named route def test_assert_redirect_to_named_route with_routing do |set| - set.draw do - set.route_one 'route_one', :controller => 'action_pack_assertions', :action => 'nothing' - set.connect ':controller/:action/:id' + set.draw do |map| + map.route_one 'route_one', :controller => 'action_pack_assertions', :action => 'nothing' + map.connect ':controller/:action/:id' end + set.named_routes.install + process :redirect_to_named_route assert_redirected_to 'http://test.host/route_one' assert_redirected_to route_one_url @@ -240,10 +242,10 @@ class ActionPackAssertionsControllerTest < Test::Unit::TestCase def test_assert_redirect_to_named_route_failure with_routing do |set| - set.draw do - set.route_one 'route_one', :controller => 'action_pack_assertions', :action => 'nothing', :id => 'one' - set.route_two 'route_two', :controller => 'action_pack_assertions', :action => 'nothing', :id => 'two' - set.connect ':controller/:action/:id' + set.draw do |map| + map.route_one 'route_one', :controller => 'action_pack_assertions', :action => 'nothing', :id => 'one' + map.route_two 'route_two', :controller => 'action_pack_assertions', :action => 'nothing', :id => 'two' + map.connect ':controller/:action/:id' end process :redirect_to_named_route assert_raise(Test::Unit::AssertionFailedError) do diff --git a/actionpack/test/controller/controller_fixtures/app/controllers/admin/user_controller.rb b/actionpack/test/controller/controller_fixtures/app/controllers/admin/user_controller.rb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/actionpack/test/controller/controller_fixtures/app/controllers/user_controller.rb b/actionpack/test/controller/controller_fixtures/app/controllers/user_controller.rb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/actionpack/test/controller/controller_fixtures/vendor/plugins/bad_plugin/lib/plugin_controller.rb b/actionpack/test/controller/controller_fixtures/vendor/plugins/bad_plugin/lib/plugin_controller.rb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/actionpack/test/controller/mime_type_test.rb b/actionpack/test/controller/mime_type_test.rb index aa1d4459ee..49869d647e 100644 --- a/actionpack/test/controller/mime_type_test.rb +++ b/actionpack/test/controller/mime_type_test.rb @@ -5,6 +5,7 @@ class MimeTypeTest < Test::Unit::TestCase Mime::PLAIN = Mime::Type.new("text/plain") def test_parse_single +p Mime::LOOKUP.keys.sort Mime::LOOKUP.keys.each do |mime_type| assert_equal [Mime::Type.lookup(mime_type)], Mime::Type.parse(mime_type) end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 4f094a36ff..3f8397c95e 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -1,647 +1,41 @@ require File.dirname(__FILE__) + '/../abstract_unit' require 'test/unit' require File.dirname(__FILE__) + '/fake_controllers' -require 'stringio' +require 'action_controller/routing' RunTimeTests = ARGV.include? 'time' +ROUTING = ActionController::Routing -module ActionController::CodeGeneration +class ROUTING::RouteBuilder + attr_reader :warn_output -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 - -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, "::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 => ::ContentController, :action => 'index'}, execute('hello/world/how/are/you')) - end - - def test_basic_dynamic - c = [Static.new("hi"), Dynamic.new(:action)] - g.result :controller, "::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 => ::ContentController, :action => 'dude'}, execute('hi/dude')) - end - - def test_basic_dynamic_backwards - c = [Dynamic.new(:action), Static.new("hi")] - go c - - assert_nil execute('') - assert_nil execute('boo') - assert_nil execute('boo/blah') - assert_nil execute('hi') - assert_equal({:action => 'index'}, execute('index/hi')) - assert_equal({:action => 'show'}, execute('show/hi')) - assert_nil execute('hi/dude') - end - - def test_dynamic_with_default - c = [Static.new("hi"), Dynamic.new(:action, :default => 'index')] - g.result :controller, "::ContentController", true - go c - - assert_nil execute('boo') - assert_nil execute('boo/blah') - assert_nil execute('hi/dude/what') - assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hi')) - assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hi/index')) - assert_equal({:controller => ::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, "::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 => ::ContentController, :action => 'index'}, execute('hi/index')) - assert_nil execute('hi/dude') - end - - def test_dynamic_with_string_condition_backwards - c = [Dynamic.new(:action, :condition => 'index'), Static.new("hi")] - g.result :controller, "::ContentController", true - go c - - assert_nil execute('boo') - assert_nil execute('boo/blah') - assert_nil execute('hi') - assert_nil execute('dude/what/hi') - assert_nil execute('index/what') - assert_equal({:controller => ::ContentController, :action => 'index'}, execute('index/hi')) - assert_nil execute('dude/hi') - end - - def test_dynamic_with_regexp_condition - c = [Static.new("hi"), Dynamic.new(:action, :condition => /^[a-z]+$/)] - g.result :controller, "::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 => ::ContentController, :action => 'index'}, execute('hi/index')) - assert_equal({:controller => ::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, "::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 => ::ContentController, :action => 'index'}, execute('hi')) - assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hi/index')) - assert_equal({:controller => ::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, "::ContentController", true - g.constant_result :action, "download" - - go c - - assert_nil execute('boo') - assert_nil execute('boo/blah') - assert_equal({:controller => ::ContentController, :action => 'download', :file => []}, execute('hi')) - assert_equal({:controller => ::ContentController, :action => 'download', :file => %w(books agile_rails_dev.pdf)}, - execute('hi/books/agile_rails_dev.pdf')) - assert_equal({:controller => ::ContentController, :action => 'download', :file => ['dude']}, execute('hi/dude')) - assert_equal 'dude/what', execute('hi/dude/what')[:file].to_s - end - - def test_path_with_dynamic - c = [Dynamic.new(:action), Path.new(:file)] - g.result :controller, "::ContentController", true - - go c - - assert_nil execute('') - assert_equal({:controller => ::ContentController, :action => 'download', :file => []}, execute('download')) - assert_equal({:controller => ::ContentController, :action => 'download', :file => %w(books agile_rails_dev.pdf)}, - execute('download/books/agile_rails_dev.pdf')) - assert_equal({:controller => ::ContentController, :action => 'download', :file => ['dude']}, execute('download/dude')) - assert_equal 'dude/what', execute('hi/dude/what')[:file].to_s - end - - def test_path_with_dynamic_and_default - c = [Dynamic.new(:action, :default => 'index'), Path.new(:file)] - - go c - - assert_equal({:action => 'index', :file => []}, execute('')) - assert_equal({:action => 'index', :file => []}, execute('index')) - assert_equal({:action => 'blarg', :file => []}, execute('blarg')) - assert_equal({:action => 'index', :file => ['content']}, execute('index/content')) - assert_equal({:action => 'show', :file => ['rails_dev.pdf']}, execute('show/rails_dev.pdf')) - 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 => ::ContentController, :action => 'hi'}, execute('hi/content')) - assert_equal({:controller => ::Admin::UserController, :action => 'hi'}, execute('hi/admin/user')) - end - - def test_controller_with_regexp - c = [Static.new("hi"), Controller.new(:controller, :condition => /^admin\/.+$/)] - g.constant_result :action, "hi" - - go c - - assert_nil execute('hi') - assert_nil execute('hi/x') - assert_nil execute('hi/content') - assert_equal({:controller => ::Admin::UserController, :action => 'hi'}, execute('hi/admin/user')) - assert_equal({:controller => ::Admin::NewsFeedController, :action => 'hi'}, execute('hi/admin/news_feed')) - assert_nil execute('hi/admin/user/foo') - 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 => ::ContentController, :action => 'index'}, execute('content')) - assert_equal({:controller => ::ContentController, :action => 'list'}, execute('content/list')) - assert_equal({:controller => ::ContentController, :action => 'show', :id => '10'}, execute('content/show/10')) - - assert_equal({:controller => ::Admin::UserController, :action => 'index'}, execute('admin/user')) - assert_equal({:controller => ::Admin::UserController, :action => 'list'}, execute('admin/user/list')) - assert_equal({:controller => ::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, "::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 => ::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_controller_with_regexp - c = [Static.new("hi"), Controller.new(:controller, :condition => /^admin\/.+$/)] - go c - - assert_nil execute({}, {}) - assert_nil execute({:controller => 'content'}, {}) - assert_equal '/hi/admin/user', execute({:controller => 'admin/user'}, {}) - assert_nil 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) - @route = ::ActionController::Routing::Route.new(*args) unless args.empty? - return @route - end - - def rec(path, show = false) - path = path.split('/') if path.is_a? String - index = 0 - source = route.write_recognition.to_s - puts "\n\n#{source}\n\n" if show - r = eval(source) - r ? r.symbolize_keys : r - end - def gen(options, recall = nil, show = false) - recall ||= options.dup - - expire_on = ::ActionController::Routing.expiry_hash(options, recall) - hash = merged = recall.merge(options) - not_expired = true - - source = route.write_generation.to_s - puts "\n\n#{source}\n\n" if show - eval(source) - - end - - def test_static - route 'hello/world', :known => 'known_value', :controller => 'content', :action => 'index' - - assert_nil rec('hello/turn') - assert_nil rec('turn/world') - assert_equal( - {:known => 'known_value', :controller => ::ContentController, :action => 'index'}, - rec('hello/world') - ) - - assert_nil gen(:known => 'foo') - assert_nil gen({}) - assert_equal '/hello/world', gen(:known => 'known_value', :controller => 'content', :action => 'index') - assert_equal '/hello/world', gen(:known => 'known_value', :extra => 'hi', :controller => 'content', :action => 'index') - assert_equal [:extra], route.extra_keys(:known => 'known_value', :extra => 'hi') - end - - def test_dynamic - route 'hello/:name', :controller => 'content', :action => 'show_person' - - assert_nil rec('hello') - assert_nil rec('foo/bar') - assert_equal({:controller => ::ContentController, :action => 'show_person', :name => 'rails'}, rec('hello/rails')) - assert_equal({:controller => ::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_typical - route ':controller/:action/:id', :action => 'index', :id => nil - assert_nil rec('hello') - assert_nil rec('foo bar') - assert_equal({:controller => ::ContentController, :action => 'index'}, rec('content')) - assert_equal({:controller => ::Admin::UserController, :action => 'index'}, rec('admin/user')) - - assert_equal({:controller => ::Admin::UserController, :action => 'index'}, rec('admin/user/index')) - assert_equal({:controller => ::Admin::UserController, :action => 'list'}, rec('admin/user/list')) - assert_equal({:controller => ::Admin::UserController, :action => 'show', :id => '10'}, rec('admin/user/show/10')) - - assert_equal({:controller => ::ContentController, :action => 'list'}, rec('content/list')) - assert_equal({:controller => ::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') + def warn(msg) + (@warn_output ||= []) << msg end end -class RouteSetTests < Test::Unit::TestCase +class LegacyRouteSetTests < Test::Unit::TestCase attr_reader :rs def setup @rs = ::ActionController::Routing::RouteSet.new + ActionController::Routing.use_controllers! %w(content admin/user admin/news_feed) @rs.draw {|m| m.connect ':controller/:action/:id' } - ::ActionController::Routing::NamedRoutes.clear end def test_default_setup - assert_equal({:controller => ::ContentController, :action => 'index'}.stringify_keys, rs.recognize_path(%w(content))) - assert_equal({:controller => ::ContentController, :action => 'list'}.stringify_keys, rs.recognize_path(%w(content list))) - assert_equal({:controller => ::ContentController, :action => 'show', :id => '10'}.stringify_keys, rs.recognize_path(%w(content show 10))) + assert_equal({:controller => "content", :action => 'index'}, rs.recognize_path("/content")) + assert_equal({:controller => "content", :action => 'list'}, rs.recognize_path("/content/list")) + assert_equal({:controller => "content", :action => 'show', :id => '10'}, rs.recognize_path("/content/show/10")) - assert_equal({:controller => ::Admin::UserController, :action => 'show', :id => '10'}.stringify_keys, rs.recognize_path(%w(admin user show 10))) + assert_equal({:controller => "admin/user", :action => 'show', :id => '10'}, rs.recognize_path("/admin/user/show/10")) - assert_equal ['/admin/user/show/10', []], rs.generate({:controller => 'admin/user', :action => 'show', :id => 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'}) + 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'}) - assert_equal ['/admin/stuff', []], rs.generate({:controller => 'stuff'}, {:controller => 'admin/user', :action => 'list', :id => '10'}) - assert_equal ['/stuff', []], rs.generate({:controller => '/stuff'}, {:controller => 'admin/user', :action => 'list', :id => '10'}) + assert_equal '/admin/stuff', rs.generate({:controller => 'stuff'}, {:controller => 'admin/user', :action => 'list', :id => '10'}) + assert_equal '/stuff', rs.generate({:controller => '/stuff'}, {:controller => 'admin/user', :action => 'list', :id => '10'}) end def test_ignores_leading_slash @@ -655,12 +49,12 @@ class RouteSetTests < Test::Unit::TestCase 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)) + rs.recognize_path("content") + rs.recognize_path("content/list") + rs.recognize_path("content/show/10") + rs.recognize_path("admin/user") + rs.recognize_path("admin/user/list") + rs.recognize_path("admin/user/show/10") end end puts "\n\nRecognition (RouteSet):" @@ -704,86 +98,74 @@ class RouteSetTests < Test::Unit::TestCase end end - def test_route_generating_string_literal_in_comparison_warning - old_stderr = $stderr - $stderr = StringIO.new - rs.draw do |map| - map.connect 'subscriptions/:action/:subscription_type', :controller => "subscriptions" - end - assert_equal "", $stderr.string - ensure - $stderr = old_stderr - end - def test_route_with_regexp_for_controller rs.draw do |map| map.connect ':controller/:admintoken/:action/:id', :controller => /admin\/.+/ map.connect ':controller/:action/:id' end - assert_equal({:controller => ::Admin::UserController, :admintoken => "foo", :action => "index"}.stringify_keys, - rs.recognize_path(%w(admin user foo))) - assert_equal({:controller => ::ContentController, :action => "foo"}.stringify_keys, - rs.recognize_path(%w(content foo))) - assert_equal ['/admin/user/foo', []], rs.generate(:controller => "admin/user", :admintoken => "foo", :action => "index") - assert_equal ['/content/foo',[]], rs.generate(:controller => "content", :action => "foo") + assert_equal({:controller => "admin/user", :admintoken => "foo", :action => "index"}, + rs.recognize_path("/admin/user/foo")) + assert_equal({:controller => "content", :action => "foo"}, rs.recognize_path("/content/foo")) + assert_equal '/admin/user/foo', rs.generate(:controller => "admin/user", :admintoken => "foo", :action => "index") + assert_equal '/content/foo', rs.generate(:controller => "content", :action => "foo") end 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)) + rs.add_named_route :home, '', :controller => 'content', :action => 'list' + x = setup_for_named_route.new + assert_equal({:controller => 'content', :action => 'list', :use_route => :home}, + x.send(:home_url)) 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')) + rs.add_named_route :page, 'page/:title', :controller => 'content', :action => 'show_page' + x = setup_for_named_route.new + assert_equal({:controller => 'content', :action => 'show_page', :title => 'new stuff', :use_route => :page}, + x.send(:page_url, :title => 'new stuff')) end def test_named_route_with_default - rs.page 'page/:title', :controller => 'content', :action => 'show_page', :title => 'AboutPage' - x = setup_for_named_route - assert_equal({:controller => '/content', :action => 'show_page', :title => 'AboutPage'}, - x.new.send(:page_url)) - assert_equal({:controller => '/content', :action => 'show_page', :title => 'AboutRails'}, - x.new.send(:page_url, :title => "AboutRails")) + rs.add_named_route :page, 'page/:title', :controller => 'content', :action => 'show_page', :title => 'AboutPage' + x = setup_for_named_route.new + assert_equal({:controller => 'content', :action => 'show_page', :title => 'AboutPage', :use_route => :page}, + x.send(:page_url)) + assert_equal({:controller => 'content', :action => 'show_page', :title => 'AboutRails', :use_route => :page}, + x.send(:page_url, :title => "AboutRails")) end def setup_for_named_route x = Class.new x.send(:define_method, :url_for) {|x| x} - x.send :include, ::ActionController::Routing::NamedRoutes + rs.named_routes.install(x) x end def test_named_route_without_hash rs.draw do |map| - rs.normal ':controller/:action/:id' + map.normal ':controller/:action/:id' end end def test_named_route_with_regexps rs.draw do |map| - rs.article 'page/:year/:month/:day/:title', :controller => 'page', :action => 'show', + map.article 'page/:year/:month/:day/:title', :controller => 'page', :action => 'show', :year => /^\d+$/, :month => /^\d+$/, :day => /^\d+$/ - rs.connect ':controller/:action/:id' + map.connect ':controller/:action/:id' end - x = setup_for_named_route + x = setup_for_named_route.new assert_equal( - {:controller => '/page', :action => 'show', :title => 'hi'}, - x.new.send(:article_url, :title => 'hi') + {:controller => 'page', :action => 'show', :title => 'hi', :use_route => :article}, + x.send(:article_url, :title => 'hi') ) assert_equal( - {:controller => '/page', :action => 'show', :title => 'hi', :day => 10, :year => 2005, :month => 6}, - x.new.send(:article_url, :title => 'hi', :day => 10, :year => 2005, :month => 6) + {:controller => 'page', :action => 'show', :title => 'hi', :day => 10, :year => 2005, :month => 6, :use_route => :article}, + x.send(:article_url, :title => 'hi', :day => 10, :year => 2005, :month => 6) ) end def test_changing_controller - assert_equal ['/admin/stuff/show/10', []], rs.generate( + assert_equal '/admin/stuff/show/10', rs.generate( {:controller => 'stuff', :action => 'show', :id => 10}, {:controller => 'admin/user', :action => 'index'} ) @@ -791,192 +173,189 @@ class RouteSetTests < Test::Unit::TestCase def test_paths_escaped rs.draw do |map| - rs.path 'file/*path', :controller => 'content', :action => 'show_file' - rs.connect ':controller/:action/:id' + map.path 'file/*path', :controller => 'content', :action => 'show_file' + map.connect ':controller/:action/:id' end - results = rs.recognize_path %w(file hello+world how+are+you%3F) + results = rs.recognize_path "/file/hello+world/how+are+you%3F" assert results, "Recognition should have succeeded" - assert_equal ['hello world', 'how are you?'], results['path'] + assert_equal ['hello world', 'how are you?'], results[:path] - results = rs.recognize_path %w(file) + results = rs.recognize_path "/file" assert results, "Recognition should have succeeded" - assert_equal [], results['path'] + assert_equal [], results[:path] end def test_non_controllers_cannot_be_matched - rs.draw do - rs.connect ':controller/:action/:id' + rs.draw do |map| + map.connect ':controller/:action/:id' end - assert_nil rs.recognize_path(%w(not_a show 10)), "Shouldn't recognize non-controllers as controllers!" + assert_raises(ActionController::RoutingError) { rs.recognize_path("/not_a/show/10") } end def test_paths_do_not_accept_defaults assert_raises(ActionController::RoutingError) do rs.draw do |map| - rs.path 'file/*path', :controller => 'content', :action => 'show_file', :path => %w(fake default) - rs.connect ':controller/:action/:id' + map.path 'file/*path', :controller => 'content', :action => 'show_file', :path => %w(fake default) + map.connect ':controller/:action/:id' end end rs.draw do |map| - rs.path 'file/*path', :controller => 'content', :action => 'show_file', :path => [] - rs.connect ':controller/:action/:id' + map.path 'file/*path', :controller => 'content', :action => 'show_file', :path => [] + map.connect ':controller/:action/:id' end end def test_dynamic_path_allowed rs.draw do |map| - rs.connect '*path', :controller => 'content', :action => 'show_file' + map.connect '*path', :controller => 'content', :action => 'show_file' end - assert_equal ['/pages/boo', []], rs.generate(:controller => 'content', :action => 'show_file', :path => %w(pages boo)) + assert_equal '/pages/boo', rs.generate(:controller => 'content', :action => 'show_file', :path => %w(pages boo)) end def test_backwards rs.draw do |map| - rs.connect 'page/:id/:action', :controller => 'pages', :action => 'show' - rs.connect ':controller/:action/:id' + map.connect 'page/:id/:action', :controller => 'pages', :action => 'show' + map.connect ':controller/:action/:id' end - assert_equal ['/page/20', []], rs.generate({:id => 20}, {:controller => 'pages'}) - assert_equal ['/page/20', []], rs.generate(:controller => 'pages', :id => 20, :action => 'show') - assert_equal ['/pages/boo', []], rs.generate(:controller => 'pages', :action => 'boo') + assert_equal '/page/20', rs.generate({:id => 20}, {:controller => 'pages', :action => 'show'}) + assert_equal '/page/20', rs.generate(:controller => 'pages', :id => 20, :action => 'show') + assert_equal '/pages/boo', rs.generate(:controller => 'pages', :action => 'boo') end def test_route_with_fixnum_default rs.draw do |map| - rs.connect 'page/:id', :controller => 'content', :action => 'show_page', :id => 1 - rs.connect ':controller/:action/:id' + map.connect 'page/:id', :controller => 'content', :action => 'show_page', :id => 1 + map.connect ':controller/:action/:id' end - assert_equal ['/page', []], rs.generate(:controller => 'content', :action => 'show_page') - assert_equal ['/page', []], rs.generate(:controller => 'content', :action => 'show_page', :id => 1) - assert_equal ['/page', []], rs.generate(:controller => 'content', :action => 'show_page', :id => '1') - assert_equal ['/page/10', []], rs.generate(:controller => 'content', :action => 'show_page', :id => 10) - - ctrl = ::ContentController + assert_equal '/page', rs.generate(:controller => 'content', :action => 'show_page') + assert_equal '/page', rs.generate(:controller => 'content', :action => 'show_page', :id => 1) + assert_equal '/page', rs.generate(:controller => 'content', :action => 'show_page', :id => '1') + assert_equal '/page/10', rs.generate(:controller => 'content', :action => 'show_page', :id => 10) - assert_equal({'controller' => ctrl, 'action' => 'show_page', 'id' => 1}, rs.recognize_path(%w(page))) - assert_equal({'controller' => ctrl, 'action' => 'show_page', 'id' => '1'}, rs.recognize_path(%w(page 1))) - assert_equal({'controller' => ctrl, 'action' => 'show_page', 'id' => '10'}, rs.recognize_path(%w(page 10))) + assert_equal({:controller => "content", :action => 'show_page', :id => '1'}, rs.recognize_path("/page")) + assert_equal({:controller => "content", :action => 'show_page', :id => '1'}, rs.recognize_path("/page/1")) + assert_equal({:controller => "content", :action => 'show_page', :id => '10'}, rs.recognize_path("/page/10")) end def test_action_expiry - assert_equal ['/content', []], rs.generate({:controller => 'content'}, {:controller => 'content', :action => 'show'}) + assert_equal '/content', rs.generate({:controller => 'content'}, {:controller => 'content', :action => 'show'}) end def test_recognition_with_uppercase_controller_name - assert_equal({'controller' => ::ContentController, 'action' => 'index'}, rs.recognize_path(%w(Content))) - assert_equal({'controller' => ::ContentController, 'action' => 'list'}, rs.recognize_path(%w(Content list))) - assert_equal({'controller' => ::ContentController, 'action' => 'show', 'id' => '10'}, rs.recognize_path(%w(Content show 10))) + assert_equal({:controller => "content", :action => 'index'}, rs.recognize_path("/Content")) + assert_equal({:controller => "content", :action => 'list'}, rs.recognize_path("/ConTent/list")) + assert_equal({:controller => "content", :action => 'show', :id => '10'}, rs.recognize_path("/CONTENT/show/10")) - assert_equal({'controller' => ::Admin::NewsFeedController, 'action' => 'index'}, rs.recognize_path(%w(Admin NewsFeed))) - assert_equal({'controller' => ::Admin::NewsFeedController, 'action' => 'index'}, rs.recognize_path(%w(Admin News_Feed))) + # these used to work, before the routes rewrite, but support for this was pulled in the new version... + #assert_equal({'controller' => "admin/news_feed", 'action' => 'index'}, rs.recognize_path("Admin/NewsFeed")) + #assert_equal({'controller' => "admin/news_feed", 'action' => 'index'}, rs.recognize_path("Admin/News_Feed")) end def test_both_requirement_and_optional - rs.draw do - rs.blog('test/:year', :controller => 'post', :action => 'show', + rs.draw do |map| + map.blog('test/:year', :controller => 'post', :action => 'show', :defaults => { :year => nil }, :requirements => { :year => /\d{4}/ } ) - rs.connect ':controller/:action/:id' + map.connect ':controller/:action/:id' end - assert_equal ['/test', []], rs.generate(:controller => 'post', :action => 'show') - assert_equal ['/test', []], rs.generate(:controller => 'post', :action => 'show', :year => nil) + assert_equal '/test', rs.generate(:controller => 'post', :action => 'show') + assert_equal '/test', rs.generate(:controller => 'post', :action => 'show', :year => nil) - x = setup_for_named_route - assert_equal({:controller => '/post', :action => 'show'}, - x.new.send(:blog_url)) + x = setup_for_named_route.new + assert_equal({:controller => 'post', :action => 'show', :use_route => :blog}, + x.send(:blog_url)) end def test_set_to_nil_forgets - rs.draw do - rs.connect 'pages/:year/:month/:day', :controller => 'content', :action => 'list_pages', :month => nil, :day => nil - rs.connect ':controller/:action/:id' + rs.draw do |map| + map.connect 'pages/:year/:month/:day', :controller => 'content', :action => 'list_pages', :month => nil, :day => nil + map.connect ':controller/:action/:id' end - assert_equal ['/pages/2005', []], + assert_equal '/pages/2005', rs.generate(:controller => 'content', :action => 'list_pages', :year => 2005) - assert_equal ['/pages/2005/6', []], + assert_equal '/pages/2005/6', rs.generate(:controller => 'content', :action => 'list_pages', :year => 2005, :month => 6) - assert_equal ['/pages/2005/6/12', []], + assert_equal '/pages/2005/6/12', rs.generate(:controller => 'content', :action => 'list_pages', :year => 2005, :month => 6, :day => 12) - assert_equal ['/pages/2005/6/4', []], + assert_equal '/pages/2005/6/4', rs.generate({:day => 4}, {:controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12'}) - assert_equal ['/pages/2005/6', []], + assert_equal '/pages/2005/6', rs.generate({:day => nil}, {:controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12'}) - assert_equal ['/pages/2005', []], + assert_equal '/pages/2005', rs.generate({:day => nil, :month => nil}, {:controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12'}) end def test_url_with_no_action_specified - rs.draw do - rs.connect '', :controller => 'content' - rs.connect ':controller/:action/:id' + rs.draw do |map| + map.connect '', :controller => 'content' + map.connect ':controller/:action/:id' end - assert_equal ['/', []], rs.generate(:controller => 'content', :action => 'index') - assert_equal ['/', []], rs.generate(:controller => 'content') + assert_equal '/', rs.generate(:controller => 'content', :action => 'index') + assert_equal '/', rs.generate(:controller => 'content') end def test_named_url_with_no_action_specified - rs.draw do - rs.root '', :controller => 'content' - rs.connect ':controller/:action/:id' + rs.draw do |map| + map.root '', :controller => 'content' + map.connect ':controller/:action/:id' end - assert_equal ['/', []], rs.generate(:controller => 'content', :action => 'index') - assert_equal ['/', []], rs.generate(:controller => 'content') + assert_equal '/', rs.generate(:controller => 'content', :action => 'index') + assert_equal '/', rs.generate(:controller => 'content') - x = setup_for_named_route - assert_equal({:controller => '/content', :action => 'index'}, - x.new.send(:root_url)) + x = setup_for_named_route.new + assert_equal({:controller => 'content', :action => 'index', :use_route => :root}, + x.send(:root_url)) end def test_url_generated_when_forgetting_action [{:controller => 'content', :action => 'index'}, {:controller => 'content'}].each do |hash| - rs.draw do - rs.root '', hash - rs.connect ':controller/:action/:id' + rs.draw do |map| + map.root '', hash + map.connect ':controller/:action/:id' end - assert_equal ['/', []], rs.generate({:action => nil}, {:controller => 'content', :action => 'hello'}) - assert_equal ['/', []], rs.generate({:controller => 'content'}) - assert_equal ['/content/hi', []], rs.generate({:controller => 'content', :action => 'hi'}) + assert_equal '/', rs.generate({:action => nil}, {:controller => 'content', :action => 'hello'}) + assert_equal '/', rs.generate({:controller => 'content'}) + assert_equal '/content/hi', rs.generate({:controller => 'content', :action => 'hi'}) end end def test_named_route_method - rs.draw do - assert_raises(ArgumentError) { rs.categories 'categories', :controller => 'content', :action => 'categories' } - - rs.named_route :categories, 'categories', :controller => 'content', :action => 'categories' - rs.connect ':controller/:action/:id' + rs.draw do |map| + map.categories 'categories', :controller => 'content', :action => 'categories' + map.connect ':controller/:action/:id' end - assert_equal ['/categories', []], rs.generate(:controller => 'content', :action => 'categories') - assert_equal ['/content/hi', []], rs.generate({:controller => 'content', :action => 'hi'}) + assert_equal '/categories', rs.generate(:controller => 'content', :action => 'categories') + assert_equal '/content/hi', rs.generate({:controller => 'content', :action => 'hi'}) end - def test_named_route_helper_array + def test_named_routes_array test_named_route_method - assert_equal [:categories_url, :hash_for_categories_url], ::ActionController::Routing::NamedRoutes::Helpers + assert_equal [:categories], rs.named_routes.names end def test_nil_defaults - rs.draw do - rs.connect 'journal', + rs.draw do |map| + map.connect 'journal', :controller => 'content', :action => 'list_journal', :date => nil, :user_id => nil - rs.connect ':controller/:action/:id' + map.connect ':controller/:action/:id' end - assert_equal ['/journal', []], rs.generate(:controller => 'content', :action => 'list_journal', :date => nil, :user_id => nil) + assert_equal '/journal', rs.generate(:controller => 'content', :action => 'list_journal', :date => nil, :user_id => nil) end def setup_request_method_routes_for(method) @@ -985,10 +364,10 @@ class RouteSetTests < Test::Unit::TestCase @request.request_uri = "/match" rs.draw do |r| - r.connect '/match', :controller => 'books', :action => 'get', :require => { :method => :get } - r.connect '/match', :controller => 'books', :action => 'post', :require => { :method => :post } - r.connect '/match', :controller => 'books', :action => 'put', :require => { :method => :put } - r.connect '/match', :controller => 'books', :action => 'delete', :require => { :method => :delete } + r.connect '/match', :controller => 'books', :action => 'get', :conditions => { :method => :get } + r.connect '/match', :controller => 'books', :action => 'post', :conditions => { :method => :post } + r.connect '/match', :controller => 'books', :action => 'put', :conditions => { :method => :put } + r.connect '/match', :controller => 'books', :action => 'delete', :conditions => { :method => :delete } end end @@ -1000,7 +379,7 @@ class RouteSetTests < Test::Unit::TestCase setup_request_method_routes_for(request_method) assert_nothing_raised { rs.recognize(@request) } - assert_equal request_method.downcase, @request.path_parameters["action"] + assert_equal request_method.downcase, @request.path_parameters[:action] ensure Object.send(:remove_const, :BooksController) rescue nil end @@ -1017,29 +396,21 @@ class RouteSetTests < Test::Unit::TestCase r.connect '/posts/:id', :controller => 'subpath_books', :action => "show" end - hash = rs.recognize_path %w(books 17;edit) + hash = rs.recognize_path "/books/17;edit" assert_not_nil hash - assert_equal %w(subpath_books 17 edit), [hash["controller"].controller_name, hash["id"], hash["action"]] + assert_equal %w(subpath_books 17 edit), [hash[:controller], hash[:id], hash[:action]] - hash = rs.recognize_path %w(items 3;complete) + hash = rs.recognize_path "/items/3;complete" assert_not_nil hash - assert_equal %w(subpath_books 3 complete), [hash["controller"].controller_name, hash["id"], hash["action"]] + assert_equal %w(subpath_books 3 complete), [hash[:controller], hash[:id], hash[:action]] - hash = rs.recognize_path %w(posts new;preview) + hash = rs.recognize_path "/posts/new;preview" assert_not_nil hash - assert_equal %w(subpath_books preview), [hash["controller"].controller_name, hash["action"]] + assert_equal %w(subpath_books preview), [hash[:controller], hash[:action]] - hash = rs.recognize_path %w(posts 7) + hash = rs.recognize_path "/posts/7" assert_not_nil hash - assert_equal %w(subpath_books show 7), [hash["controller"].controller_name, hash["action"], hash["id"]] - - # for now, low-hanging fruit only. We don't allow subpath components anywhere - # except at the end of the path - assert_raises(ActionController::RoutingError) do - rs.draw do |r| - r.connect '/books;german/new', :controller => 'subpath_books', :action => "new" - end - end + assert_equal %w(subpath_books show 7), [hash[:controller], hash[:action], hash[:id]] ensure Object.send(:remove_const, :SubpathBooksController) rescue nil end @@ -1053,12 +424,892 @@ class RouteSetTests < Test::Unit::TestCase r.connect '/posts/new;:action', :controller => 'subpath_books' end - assert_equal ["/books/7;edit", []], rs.generate(:controller => "subpath_books", :id => 7, :action => "edit") - assert_equal ["/items/15;complete", []], rs.generate(:controller => "subpath_books", :id => 15, :action => "complete") - assert_equal ["/posts/new;preview", []], rs.generate(:controller => "subpath_books", :action => "preview") + assert_equal "/books/7;edit", rs.generate(:controller => "subpath_books", :id => 7, :action => "edit") + assert_equal "/items/15;complete", rs.generate(:controller => "subpath_books", :id => 15, :action => "complete") + assert_equal "/posts/new;preview", rs.generate(:controller => "subpath_books", :action => "preview") ensure Object.send(:remove_const, :SubpathBooksController) rescue nil end end +class SegmentTest < Test::Unit::TestCase + + def test_first_segment_should_interpolate_for_structure + s = ROUTING::Segment.new + def s.interpolation_statement(array) 'hello' end + assert_equal 'hello', s.continue_string_structure([]) + end + + def test_interpolation_statement + s = ROUTING::StaticSegment.new + s.value = "Hello" + assert_equal "Hello", eval(s.interpolation_statement([])) + assert_equal "HelloHello", eval(s.interpolation_statement([s])) + + s2 = ROUTING::StaticSegment.new + s2.value = "-" + assert_equal "Hello-Hello", eval(s.interpolation_statement([s, s2])) + + s3 = ROUTING::StaticSegment.new + s3.value = "World" + assert_equal "Hello-World", eval(s3.interpolation_statement([s, s2])) + end + +end + +class StaticSegmentTest < Test::Unit::TestCase + + def test_interpolation_chunk_should_respect_raw + s = ROUTING::StaticSegment.new + s.value = 'Hello/World' + assert ! s.raw? + assert_equal 'Hello/World', CGI.unescape(s.interpolation_chunk) + + s.raw = true + assert s.raw? + assert_equal 'Hello/World', s.interpolation_chunk + end + + def test_regexp_chunk_should_escape_specials + s = ROUTING::StaticSegment.new + + s.value = 'Hello*World' + assert_equal 'Hello\*World', s.regexp_chunk + + s.value = 'HelloWorld' + assert_equal 'HelloWorld', s.regexp_chunk + end + + def test_regexp_chunk_should_add_question_mark_for_optionals + s = ROUTING::StaticSegment.new + s.value = "/" + s.is_optional = true + assert_equal "/?", s.regexp_chunk + + s.value = "hello" + assert_equal "(?:hello)?", s.regexp_chunk + end + +end + +class DynamicSegmentTest < Test::Unit::TestCase + + def segment + unless @segment + @segment = ROUTING::DynamicSegment.new + @segment.key = :a + end + @segment + end + + def test_extract_value + s = ROUTING::DynamicSegment.new + s.key = :a + + hash = {:a => '10', :b => '20'} + assert_equal '10', eval(s.extract_value) + + hash = {:b => '20'} + assert_equal nil, eval(s.extract_value) + + s.default = '20' + assert_equal '20', eval(s.extract_value) + end + + def test_default_local_name + assert_equal 'a_value', segment.local_name, + "Unexpected name -- all value_check tests will fail!" + end + + def test_presence_value_check + a_value = 10 + assert eval(segment.value_check) + end + + def test_regexp_value_check_rejects_nil + segment.regexp = /\d+/ + a_value = nil + assert ! eval(segment.value_check) + end + + def test_optional_regexp_value_check_should_accept_nil + segment.regexp = /\d+/ + segment.is_optional = true + a_value = nil + assert eval(segment.value_check) + end + + def test_regexp_value_check_rejects_no_match + segment.regexp = /\d+/ + + a_value = "Hello20World" + assert ! eval(segment.value_check) + + a_value = "20Hi" + assert ! eval(segment.value_check) + end + + def test_regexp_value_check_accepts_match + segment.regexp = /\d+/ + + a_value = "30" + assert eval(segment.value_check) + end + + def test_value_check_fails_on_nil + a_value = nil + assert ! eval(segment.value_check) + end + + def test_optional_value_needs_no_check + segment.is_optional = true + a_value = nil + assert_equal nil, segment.value_check + end + + def test_regexp_value_check_should_accept_match_with_default + segment.regexp = /\d+/ + segment.default = '200' + + a_value = '100' + assert eval(segment.value_check) + end + + def test_expiry_should_not_trigger_once_expired + not_expired = false + hash = merged = {:a => 2, :b => 3} + options = {:b => 3} + expire_on = Hash.new { raise 'No!!!' } + + eval(segment.expiry_statement) + rescue RuntimeError + flunk "Expiry check should not have occured!" + end + + def test_expiry_should_occur_according_to_expire_on + not_expired = true + hash = merged = {:a => 2, :b => 3} + options = {:b => 3} + + expire_on = {:b => true, :a => false} + eval(segment.expiry_statement) + assert not_expired + assert_equal({:a => 2, :b => 3}, hash) + + expire_on = {:b => true, :a => true} + eval(segment.expiry_statement) + assert ! not_expired + assert_equal({:b => 3}, hash) + end + + def test_extraction_code_should_return_on_nil + hash = merged = {:b => 3} + options = {:b => 3} + a_value = nil + + # Local jump because of return inside eval. + assert_raises(LocalJumpError) { eval(segment.extraction_code) } + end + + def test_extraction_code_should_return_on_mismatch + segment.regexp = /\d+/ + hash = merged = {:a => 'Hi', :b => '3'} + options = {:b => '3'} + a_value = nil + + # Local jump because of return inside eval. + assert_raises(LocalJumpError) { eval(segment.extraction_code) } + end + + def test_extraction_code_should_accept_value_and_set_local + hash = merged = {:a => 'Hi', :b => '3'} + options = {:b => '3'} + a_value = nil + + eval(segment.extraction_code) + assert_equal 'Hi', a_value + end + + def test_extraction_should_work_without_value_check + segment.default = 'hi' + hash = merged = {:b => '3'} + options = {:b => '3'} + a_value = nil + + eval(segment.extraction_code) + assert_equal 'hi', a_value + end + + def test_extraction_code_should_perform_expiry + not_expired = true + hash = merged = {:a => 'Hi', :b => '3'} + options = {:b => '3'} + expire_on = {:a => true} + a_value = nil + + eval(segment.extraction_code) + assert_equal 'Hi', a_value + assert ! not_expired + assert_equal options, hash + end + + def test_interpolation_chunk_should_replace_value + a_value = 'Hi' + assert_equal a_value, eval(%("#{segment.interpolation_chunk}")) + end + + def test_value_regexp_should_be_nil_without_regexp + assert_equal nil, segment.value_regexp + end + + def test_value_regexp_should_match_exacly + segment.regexp = /\d+/ + assert_no_match segment.value_regexp, "Hello 10 World" + assert_no_match segment.value_regexp, "Hello 10" + assert_no_match segment.value_regexp, "10 World" + assert_match segment.value_regexp, "10" + end + + def test_regexp_chunk_should_return_string + segment.regexp = /\d+/ + assert_kind_of String, segment.regexp_chunk + end + +end + +class ControllerSegmentTest < Test::Unit::TestCase + + def test_regexp_should_only_match_possible_controllers + ActionController::Routing.with_controllers %w(admin/accounts admin/users account pages) do + cs = ROUTING::ControllerSegment.new :controller + regexp = %r{\A#{cs.regexp_chunk}\Z} + + ActionController::Routing.possible_controllers.each do |name| + assert_match regexp, name + assert_no_match regexp, "#{name}_fake" + + match = regexp.match name + assert_equal name, match[1] + end + end + end + +end + +class RouteTest < Test::Unit::TestCase + + def setup + @route = ROUTING::Route.new + end + + def slash_segment(is_optional = false) + returning ROUTING::DividerSegment.new('/') do |s| + s.is_optional = is_optional + end + end + + def default_route + unless @default_route + @default_route = ROUTING::Route.new + + @default_route.segments << (s = ROUTING::StaticSegment.new) + s.value = '/' + s.raw = true + + @default_route.segments << (s = ROUTING::DynamicSegment.new) + s.key = :controller + + @default_route.segments << slash_segment(:optional) + @default_route.segments << (s = ROUTING::DynamicSegment.new) + s.key = :action + s.default = 'index' + s.is_optional = true + + @default_route.segments << slash_segment(:optional) + @default_route.segments << (s = ROUTING::DynamicSegment.new) + s.key = :id + s.is_optional = true + + @default_route.segments << slash_segment(:optional) + end + @default_route + end + + def test_default_route_recognition + expected = {:controller => 'accounts', :action => 'show', :id => '10'} + assert_equal expected, default_route.recognize('/accounts/show/10') + assert_equal expected, default_route.recognize('/accounts/show/10/') + + expected[:id] = 'jamis' + assert_equal expected, default_route.recognize('/accounts/show/jamis/') + + expected.delete :id + assert_equal expected, default_route.recognize('/accounts/show') + assert_equal expected, default_route.recognize('/accounts/show/') + + expected[:action] = 'index' + assert_equal expected, default_route.recognize('/accounts/') + assert_equal expected, default_route.recognize('/accounts') + + assert_equal nil, default_route.recognize('/') + assert_equal nil, default_route.recognize('/accounts/how/goood/it/is/to/be/free') + end + + def test_default_route_should_omit_default_action + o = {:controller => 'accounts', :action => 'index'} + assert_equal '/accounts', default_route.generate(o, o, {}) + end + + def test_default_route_should_include_default_action_when_id_present + o = {:controller => 'accounts', :action => 'index', :id => '20'} + assert_equal '/accounts/index/20', default_route.generate(o, o, {}) + end + + def test_default_route_should_work_with_action_but_no_id + o = {:controller => 'accounts', :action => 'list_all'} + assert_equal '/accounts/list_all', default_route.generate(o, o, {}) + end + + def test_parameter_shell + page_url = ROUTING::Route.new + page_url.requirements = {:controller => 'pages', :action => 'show', :id => /\d+/} + assert_equal({:controller => 'pages', :action => 'show'}, page_url.parameter_shell) + end + + def test_defaults + route = ROUTING::RouteBuilder.new.build '/users/:id.:format', :controller => "users", :action => "show", :format => "html" + assert_equal( + { :controller => "users", :action => "show", :format => "html" }, + route.defaults) + end + + def test_significant_keys_for_default_route + keys = default_route.significant_keys.sort_by {|k| k.to_s } + assert_equal [:action, :controller, :id], keys + end + + def test_significant_keys + user_url = ROUTING::Route.new + user_url.segments << (s = ROUTING::StaticSegment.new) + s.value = '/' + s.raw = true + + user_url.segments << (s = ROUTING::StaticSegment.new) + s.value = 'user' + + user_url.segments << (s = ROUTING::StaticSegment.new) + s.value = '/' + s.raw = true + s.is_optional = true + + user_url.segments << (s = ROUTING::DynamicSegment.new) + s.key = :user + + user_url.segments << (s = ROUTING::StaticSegment.new) + s.value = '/' + s.raw = true + s.is_optional = true + + user_url.requirements = {:controller => 'users', :action => 'show'} + + keys = user_url.significant_keys.sort_by { |k| k.to_s } + assert_equal [:action, :controller, :user], keys + end + + def test_build_empty_query_string + assert_equal '', @route.build_query_string({}) + end + + def test_simple_build_query_string + assert_equal '?x=1&y=2', @route.build_query_string(:x => '1', :y => '2') + end + + def test_convert_ints_build_query_string + assert_equal '?x=1&y=2', @route.build_query_string(:x => 1, :y => 2) + end + + def test_escape_spaces_build_query_string + assert_equal '?x=hello+world&y=goodbye+world', @route.build_query_string(:x => 'hello world', :y => 'goodbye world') + end + + def test_expand_array_build_query_string + assert_equal '?x[]=1&x[]=2', @route.build_query_string(:x => [1, 2]) + end + + def test_escape_spaces_build_query_string_selected_keys + assert_equal '?x=hello+world', @route.build_query_string({:x => 'hello world', :y => 'goodbye world'}, [:x]) + end +end + +class RouteBuilderTest < Test::Unit::TestCase + + def builder + @bulider ||= ROUTING::RouteBuilder.new + end + + def test_segment_for_static + segment, rest = builder.segment_for 'ulysses' + assert_equal '', rest + assert_kind_of ROUTING::StaticSegment, segment + assert_equal 'ulysses', segment.value + end + + def test_segment_for_action + segment, rest = builder.segment_for ':action' + assert_equal '', rest + assert_kind_of ROUTING::DynamicSegment, segment + assert_equal :action, segment.key + assert_equal 'index', segment.default + end + + def test_segment_for_dynamic + segment, rest = builder.segment_for ':login' + assert_equal '', rest + assert_kind_of ROUTING::DynamicSegment, segment + assert_equal :login, segment.key + assert_equal nil, segment.default + assert ! segment.optional? + end + + def test_segment_for_with_rest + segment, rest = builder.segment_for ':login/:action' + assert_equal :login, segment.key + assert_equal '/:action', rest + segment, rest = builder.segment_for rest + assert_equal '/', segment.value + assert_equal ':action', rest + segment, rest = builder.segment_for rest + assert_equal :action, segment.key + assert_equal '', rest + end + + def test_segments_for + segments = builder.segments_for_route_path '/:controller/:action/:id' + + assert_kind_of ROUTING::DividerSegment, segments[0] + assert_equal '/', segments[2].value + + assert_kind_of ROUTING::DynamicSegment, segments[1] + assert_equal :controller, segments[1].key + + assert_kind_of ROUTING::DividerSegment, segments[2] + assert_equal '/', segments[2].value + + assert_kind_of ROUTING::DynamicSegment, segments[3] + assert_equal :action, segments[3].key + + assert_kind_of ROUTING::DividerSegment, segments[4] + assert_equal '/', segments[4].value + + assert_kind_of ROUTING::DynamicSegment, segments[5] + assert_equal :id, segments[5].key + end + + def test_segment_for_action + s, r = builder.segment_for(':action/something/else') + assert_equal '/something/else', r + assert_equal 'index', s.default + assert_equal :action, s.key + end + + def test_action_default_should_not_trigger_on_prefix + s, r = builder.segment_for ':action_name/something/else' + assert_equal '/something/else', r + assert_equal :action_name, s.key + assert_equal nil, s.default + end + + def test_divide_route_options + segments = builder.segments_for_route_path '/cars/:action/:person/:car/' + defaults, requirements = builder.divide_route_options(segments, + :action => 'buy', :person => /\w+/, :car => /\w+/, + :defaults => {:person => nil, :car => nil} + ) + + assert_equal({:action => 'buy', :person => nil, :car => nil}, defaults) + assert_equal({:person => /\w+/, :car => /\w+/}, requirements) + end + + def test_assign_route_options + segments = builder.segments_for_route_path '/cars/:action/:person/:car/' + defaults = {:action => 'buy', :person => nil, :car => nil} + requirements = {:person => /\w+/, :car => /\w+/} + + route_requirements = builder.assign_route_options(segments, defaults, requirements) + assert_equal({}, route_requirements) + + assert_equal :action, segments[3].key + assert_equal 'buy', segments[3].default + + assert_equal :person, segments[5].key + assert_equal %r/\w+/, segments[5].regexp + assert segments[5].optional? + + assert_equal :car, segments[7].key + assert_equal %r/\w+/, segments[7].regexp + assert segments[7].optional? + end + + def test_optional_segments_preceding_required_segments + segments = builder.segments_for_route_path '/cars/:action/:person/:car/' + defaults = {:action => 'buy', :person => nil, :car => "model-t"} + assert builder.assign_route_options(segments, defaults, {}).empty? + + 0.upto(1) { |i| assert !segments[i].optional?, "segment #{i} is optional and it shouldn't be" } + assert segments[2].optional? + + assert_equal nil, builder.warn_output # should only warn on the :person segment + end + + def test_segmentation_of_semicolon_path + segments = builder.segments_for_route_path '/books/:id;:action' + defaults = { :action => 'show' } + assert builder.assign_route_options(segments, defaults, {}).empty? + segments.each do |segment| + assert ! segment.optional? || segment.key == :action + end + end + + def test_segmentation_of_dot_path + segments = builder.segments_for_route_path '/books/:action.rss' + assert builder.assign_route_options(segments, {}, {}).empty? + assert_equal 6, segments.length # "/", "books", "/", ":action", ".", "rss" + assert !segments.any? { |seg| seg.optional? } + end + + def test_segmentation_of_dynamic_dot_path + segments = builder.segments_for_route_path '/books/:action.:format' + assert builder.assign_route_options(segments, {}, {}).empty? + assert_equal 6, segments.length # "/", "books", "/", ":action", ".", ":format" + assert !segments.any? { |seg| seg.optional? } + assert_kind_of ROUTING::DynamicSegment, segments.last + end + + def test_assignment_of_is_optional_when_default + segments = builder.segments_for_route_path '/books/:action.rss' + assert_equal segments[3].key, :action + segments[3].default = 'changes' + builder.ensure_required_segments(segments) + assert ! segments[3].optional? + end + + def test_is_optional_is_assigned_to_default_segments + segments = builder.segments_for_route_path '/books/:action' + builder.assign_route_options(segments, {:action => 'index'}, {}) + + assert_equal segments[3].key, :action + assert segments[3].optional? + assert_kind_of ROUTING::DividerSegment, segments[2] + assert segments[2].optional? + end + + # XXX is optional not being set right? + # /blah/:defaulted_segment <-- is the second slash optional? it should be. + + def test_route_build + ActionController::Routing.with_controllers %w(users pages) do + r = builder.build '/:controller/:action/:id/', :action => nil + + [0, 2, 4].each do |i| + assert_kind_of ROUTING::DividerSegment, r.segments[i] + assert_equal '/', r.segments[i].value + assert r.segments[i].optional? if i > 1 + end + + assert_kind_of ROUTING::DynamicSegment, r.segments[1] + assert_equal :controller, r.segments[1].key + assert_equal nil, r.segments[1].default + + assert_kind_of ROUTING::DynamicSegment, r.segments[3] + assert_equal :action, r.segments[3].key + assert_equal 'index', r.segments[3].default + + assert_kind_of ROUTING::DynamicSegment, r.segments[5] + assert_equal :id, r.segments[5].key + assert r.segments[5].optional? + end + end + + def test_slashes_are_implied + routes = [ + builder.build('/:controller/:action/:id/', :action => nil), + builder.build('/:controller/:action/:id', :action => nil), + builder.build(':controller/:action/:id', :action => nil), + builder.build('/:controller/:action/:id/', :action => nil) + ] + expected = routes.first.segments.length + routes.each_with_index do |route, i| + found = route.segments.length + assert_equal expected, found, "Route #{i + 1} has #{found} segments, expected #{expected}" + end + end + +end + +class RouteSetTest < Test::Unit::TestCase + class MockController + attr_accessor :routes + + def initialize(routes) + self.routes = routes + end + + def url_for(options) + path = routes.generate(options) + "http://named.route.test#{path}" + end + end + + class MockRequest + attr_accessor :path, :path_parameters, :host, :subdomains, :domain, :method + + def initialize(values={}) + values.each { |key, value| send("#{key}=", value) } + if values[:host] + subdomain, self.domain = values[:host].split(/\./, 2) + self.subdomains = [subdomain] + end + end + end + + def set + @set ||= ROUTING::RouteSet.new + end + + def request + @request ||= MockRequest.new(:host => "named.routes.test", :method => :get) + end + + def test_generate_extras + set.draw { |m| m.connect ':controller/:action/:id' } + path, extras = set.generate_extras(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world") + assert_equal "/foo/bar/15", path + assert_equal %w(that this), extras.map(&:to_s).sort + end + + def test_extra_keys + set.draw { |m| m.connect ':controller/:action/:id' } + extras = set.extra_keys(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world") + assert_equal %w(that this), extras.map(&:to_s).sort + end + + def test_draw + assert_equal 0, set.routes.size + set.draw do |map| + map.connect '/hello/world', :controller => 'a', :action => 'b' + end + assert_equal 1, set.routes.size + end + + def test_named_draw + assert_equal 0, set.routes.size + set.draw do |map| + map.hello '/hello/world', :controller => 'a', :action => 'b' + end + assert_equal 1, set.routes.size + assert_equal set.routes.first, set.named_routes[:hello] + end + + def test_later_named_routes_take_precedence + set.draw do |map| + map.hello '/hello/world', :controller => 'a', :action => 'b' + map.hello '/hello', :controller => 'a', :action => 'b' + end + assert_equal set.routes.last, set.named_routes[:hello] + end + + def setup_named_route_test + set.draw do |map| + map.show '/people/:id', :controller => 'people', :action => 'show' + map.index '/people', :controller => 'people', :action => 'index' + map.multi '/people/go/:foo/:bar/joe/:id', :controller => 'people', :action => 'multi' + end + + klass = Class.new(MockController) + set.named_routes.install(klass) + klass.new(set) + end + + def test_named_route_hash_access_method + controller = setup_named_route_test + + assert_equal( + { :controller => 'people', :action => 'show', :id => 5, :use_route => :show }, + controller.send(:hash_for_show_url, :id => 5)) + + assert_equal( + { :controller => 'people', :action => 'index', :use_route => :index }, + controller.send(:hash_for_index_url)) + end + + def test_named_route_url_method + controller = setup_named_route_test + + assert_equal "http://named.route.test/people/5", controller.send(:show_url, :id => 5) + assert_equal "http://named.route.test/people", controller.send(:index_url) + end + + def test_namd_route_url_method_with_ordered_parameters + controller = setup_named_route_test + assert_equal "http://named.route.test/people/go/7/hello/joe/5", + controller.send(:multi_url, 7, "hello", 5) + end + + def test_draw_default_route + ActionController::Routing.with_controllers(['users']) do + set.draw do |map| + map.connect '/:controller/:action/:id' + end + + assert_equal 1, set.routes.size + route = set.routes.first + + assert route.segments.last.optional? + + assert_equal '/users/show/10', set.generate(:controller => 'users', :action => 'show', :id => 10) + assert_equal '/users/index/10', set.generate(:controller => 'users', :id => 10) + + assert_equal({:controller => 'users', :action => 'index', :id => '10'}, set.recognize_path('/users/index/10')) + assert_equal({:controller => 'users', :action => 'index', :id => '10'}, set.recognize_path('/users/index/10/')) + end + end + + def test_route_with_parameter_shell + ActionController::Routing.with_controllers(['users', 'pages']) do + set.draw do |map| + map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /\d+/ + map.connect '/:controller/:action/:id' + end + + assert_equal({:controller => 'pages', :action => 'index'}, set.recognize_path('/pages')) + assert_equal({:controller => 'pages', :action => 'index'}, set.recognize_path('/pages/index')) + assert_equal({:controller => 'pages', :action => 'list'}, set.recognize_path('/pages/list')) + + assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/pages/show/10')) + assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/page/10')) + end + end + + def test_recognize_with_conditions + Object.const_set(:PeopleController, Class.new) + + set.draw do |map| + map.with_options(:controller => "people") do |people| + people.people "/people", :action => "index", :conditions => { :method => :get } + people.connect "/people", :action => "create", :conditions => { :method => :post } + people.person "/people/:id", :action => "show", :conditions => { :method => :get } + people.connect "/people/:id", :action => "update", :conditions => { :method => :put } + people.connect "/people/:id", :action => "destroy", :conditions => { :method => :delete } + end + end + + request.path = "/people" + request.method = :get + assert_nothing_raised { set.recognize(request) } + assert_equal("index", request.path_parameters[:action]) + + request.method = :post + assert_nothing_raised { set.recognize(request) } + assert_equal("create", request.path_parameters[:action]) + + request.path = "/people/5" + request.method = :get + assert_nothing_raised { set.recognize(request) } + assert_equal("show", request.path_parameters[:action]) + assert_equal("5", request.path_parameters[:id]) + + request.method = :put + assert_nothing_raised { set.recognize(request) } + assert_equal("update", request.path_parameters[:action]) + assert_equal("5", request.path_parameters[:id]) + + request.method = :delete + assert_nothing_raised { set.recognize(request) } + assert_equal("destroy", request.path_parameters[:action]) + assert_equal("5", request.path_parameters[:id]) + ensure + Object.send(:remove_const, :PeopleController) + end + + def test_recognize_with_conditions_and_format + Object.const_set(:PeopleController, Class.new) + + set.draw do |map| + map.with_options(:controller => "people") do |people| + people.person "/people/:id", :action => "show", :conditions => { :method => :get } + people.connect "/people/:id", :action => "update", :conditions => { :method => :put } + people.connect "/people/:id.:_format", :action => "show", :conditions => { :method => :get } + end + end + + request.path = "/people/5" + request.method = :get + assert_nothing_raised { set.recognize(request) } + assert_equal("show", request.path_parameters[:action]) + assert_equal("5", request.path_parameters[:id]) + + request.method = :put + assert_nothing_raised { set.recognize(request) } + assert_equal("update", request.path_parameters[:action]) + + request.path = "/people/5.png" + request.method = :get + assert_nothing_raised { set.recognize(request) } + assert_equal("show", request.path_parameters[:action]) + assert_equal("5", request.path_parameters[:id]) + assert_equal("png", request.path_parameters[:_format]) + ensure + Object.send(:remove_const, :PeopleController) + end + + def test_generate_with_default_action + set.draw do |map| + map.connect "/people", :controller => "people" + map.connect "/people/list", :controller => "people", :action => "list" + end + + url = set.generate(:controller => "people", :action => "list") + assert_equal "/people/list", url + end + + def test_generate_finds_best_fit + set.draw do |map| + map.connect "/people", :controller => "people", :action => "index" + map.connect "/ws/people", :controller => "people", :action => "index", :ws => true + end + + url = set.generate(:controller => "people", :action => "index", :ws => true) + assert_equal "/ws/people", url + end +end + +class RoutingTest < Test::Unit::TestCase + + def test_possible_controllers + true_load_paths = $LOAD_PATH.dup + + ActionController::Routing.use_controllers! nil + Object.send(:const_set, :RAILS_ROOT, File.dirname(__FILE__) + '/controller_fixtures') + + $LOAD_PATH.clear + $LOAD_PATH.concat [ + RAILS_ROOT, RAILS_ROOT + '/app/controllers', RAILS_ROOT + '/vendor/plugins/bad_plugin/lib' + ] + + assert_equal ["admin/user", "plugin", "user"], ActionController::Routing.possible_controllers.sort + ensure + if true_load_paths + $LOAD_PATH.clear + $LOAD_PATH.concat true_load_paths + end + Object.send(:remove_const, :RAILS_ROOT) rescue nil + end + + def test_with_controllers + c = %w(admin/accounts admin/users account pages) + ActionController::Routing.with_controllers c do + assert_equal c, ActionController::Routing.possible_controllers + end + end + end diff --git a/actionpack/test/controller/test_test.rb b/actionpack/test/controller/test_test.rb index 2c341e751b..58ed819e12 100644 --- a/actionpack/test/controller/test_test.rb +++ b/actionpack/test/controller/test_test.rb @@ -81,6 +81,7 @@ HTML @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new ActionController::Routing::Routes.reload + ActionController::Routing.use_controllers! %w(content admin/user) end def teardown @@ -317,9 +318,9 @@ HTML def test_array_path_parameter_handled_properly with_routing do |set| - set.draw do - set.connect 'file/*path', :controller => 'test_test/test', :action => 'test_params' - set.connect ':controller/:action/:id' + set.draw do |map| + map.connect 'file/*path', :controller => 'test_test/test', :action => 'test_params' + map.connect ':controller/:action/:id' end get :test_params, :path => ['hello', 'world'] @@ -440,9 +441,9 @@ HTML protected def with_foo_routing with_routing do |set| - set.draw do - set.generate_url 'foo', :controller => 'test' - set.connect ':controller/:action/:id' + set.draw do |map| + map.generate_url 'foo', :controller => 'test' + map.connect ':controller/:action/:id' end yield set end diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb index 1219abdd9f..57a38f2a35 100644 --- a/actionpack/test/controller/url_rewriter_test.rb +++ b/actionpack/test/controller/url_rewriter_test.rb @@ -6,23 +6,6 @@ class UrlRewriterTests < Test::Unit::TestCase @params = {} @rewriter = ActionController::UrlRewriter.new(@request, @params) end - - def test_simple_build_query_string - assert_query_equal '?x=1&y=2', @rewriter.send(:build_query_string, :x => '1', :y => '2') - end - def test_convert_ints_build_query_string - assert_query_equal '?x=1&y=2', @rewriter.send(:build_query_string, :x => 1, :y => 2) - end - def test_escape_spaces_build_query_string - assert_query_equal '?x=hello+world&y=goodbye+world', @rewriter.send(:build_query_string, :x => 'hello world', :y => 'goodbye world') - end - def test_expand_array_build_query_string - assert_query_equal '?x[]=1&x[]=2', @rewriter.send(:build_query_string, :x => [1, 2]) - end - - def test_escape_spaces_build_query_string_selected_keys - assert_query_equal '?x=hello+world', @rewriter.send(:build_query_string, {:x => 'hello world', :y => 'goodbye world'}, [:x]) - end def test_overwrite_params @params[:controller] = 'hi' -- cgit v1.2.3