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/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 +- 6 files changed, 892 insertions(+), 861 deletions(-) delete mode 100644 actionpack/lib/action_controller/code_generation.rb (limited to 'actionpack/lib') 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 -- cgit v1.2.3