aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack
diff options
context:
space:
mode:
authorMichael Koziarski <michael@koziarski.com>2008-01-17 22:45:54 +0000
committerMichael Koziarski <michael@koziarski.com>2008-01-17 22:45:54 +0000
commit3845de022b080642e87308471376911165506133 (patch)
treea31e006ca82831f725e0a782e99ab52f911e8f6e /actionpack
parent3b9bcf13f9589423947a7d9598545e3ce8c3fb44 (diff)
downloadrails-3845de022b080642e87308471376911165506133.tar.gz
rails-3845de022b080642e87308471376911165506133.tar.bz2
rails-3845de022b080642e87308471376911165506133.zip
Restructure routing into several smaller files. References #10835 [oleganza]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@8652 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'actionpack')
-rw-r--r--actionpack/lib/action_controller/routing.rb1186
-rw-r--r--actionpack/lib/action_controller/routing/builder.rb197
-rw-r--r--actionpack/lib/action_controller/routing/optimisations.rb (renamed from actionpack/lib/action_controller/routing_optimisation.rb)0
-rw-r--r--actionpack/lib/action_controller/routing/route.rb240
-rw-r--r--actionpack/lib/action_controller/routing/route_set.rb439
-rw-r--r--actionpack/lib/action_controller/routing/routing_ext.rb46
-rw-r--r--actionpack/lib/action_controller/routing/segments.rb268
7 files changed, 1197 insertions, 1179 deletions
diff --git a/actionpack/lib/action_controller/routing.rb b/actionpack/lib/action_controller/routing.rb
index 8260a7b04c..ca04879810 100644
--- a/actionpack/lib/action_controller/routing.rb
+++ b/actionpack/lib/action_controller/routing.rb
@@ -1,53 +1,12 @@
require 'cgi'
require 'uri'
require 'action_controller/polymorphic_routes'
-require 'action_controller/routing_optimisation'
-
-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 #:nodoc:
- 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
+require 'action_controller/routing/optimisations'
+require 'action_controller/routing/routing_ext'
+require 'action_controller/routing/route'
+require 'action_controller/routing/segments'
+require 'action_controller/routing/builder'
+require 'action_controller/routing/route_set'
module ActionController
# == Routing
@@ -351,1138 +310,7 @@ module ActionController
end
end
end
-
- class Route #:nodoc:
- attr_accessor :segments, :requirements, :conditions, :optimise
-
- def initialize
- @segments = []
- @requirements = {}
- @conditions = {}
- @optimise = true
- end
-
- # Indicates whether the routes should be optimised with the string interpolation
- # version of the named routes methods.
- def optimise?
- @optimise && ActionController::Base::optimise_named_routes
- end
-
- def segment_keys
- segments.collect do |segment|
- segment.key if segment.respond_to? :key
- end.compact
- end
-
- # Write and compile a +generate+ method for this Route.
- def write_generation
- # Build the main body of the generation
- body = "expired = false\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.
- raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
- instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
-
- # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
- # are the same as the keys that were recalled from the previous request. Thus,
- # we can use the expire_on.keys to determine which keys ought to be used to build
- # the query string. (Never use keys from the recalled request when building the
- # query string.)
-
- method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend"
- instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
-
- method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend"
- instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
- raw_method
- 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[:#{key}] == #{req.inspect}"
- end
- end
- requirement_conditions * ' && ' unless requirement_conditions.empty?
- end
-
- def generation_structure
- segments.last.string_structure segments[0..-2]
- end
-
- # 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"
- instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
- method_decl
- end
-
- # 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
-
- # 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
-
- # 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
-
- # Write the real generation implementation and then resend the message.
- def generate(options, hash, expire_on = {})
- write_generation
- generate options, hash, expire_on
- end
-
- def generate_extras(options, hash, expire_on = {})
- write_generation
- generate_extras options, hash, expire_on
- end
-
- # 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, query_keys=nil)
- return nil unless path
- query_keys ||= extra_keys(hash)
- "#{path}#{build_query_string(hash, query_keys)}"
- end
-
- # Determine which keys in the given hash are "extra". Extra keys are
- # those that were not used to generate a particular route. The extra
- # keys also do not include those recalled from the prior request, nor
- # do they include any keys that were implied in the route (like a
- # :controller that is required, but not explicitly used in the text of
- # the route.)
- def extra_keys(hash, recall={})
- (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys
- end
-
- # 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 = []
-
- (only_keys || hash.keys).each do |key|
- if value = hash[key]
- elements << value.to_query(key)
- end
- end
-
- elements.empty? ? '' : "?#{elements.sort * '&'}"
- 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
-
- # 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 defined? @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 to_s
- @to_s ||= begin
- segs = segments.inject("") { |str,s| str << s.to_s }
- "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect]
- end
- 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 Segment #:nodoc:
- RESERVED_PCHAR = ':@&=+$,;'
- UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze
-
- attr_accessor :is_optional
- alias_method :optional?, :is_optional
-
- def initialize
- self.is_optional = false
- end
-
- 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 interpolation_chunk
- URI.escape(value, UNSAFE_PCHAR)
- end
-
- # 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 StaticSegment < Segment #:nodoc:
- attr_accessor :value, :raw
- alias_method :raw?, :raw
-
- def initialize(value = nil)
- super()
- self.value = value
- end
-
- def interpolation_chunk
- raw? ? value : super
- end
-
- 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
- escaped + pattern
- end
- end
-
- def to_s
- value
- end
- end
-
- class DividerSegment < StaticSegment #:nodoc:
- def initialize(value = nil)
- super(value)
- self.raw = true
- self.is_optional = true
- end
-
- def optionality_implied?
- true
- end
- end
-
- class DynamicSegment < Segment #:nodoc:
- 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 to_s
- ":#{key}"
- end
-
- # 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}] && hash[:#{key}].to_param #{"|| #{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
- end
- def expiry_statement
- "expired, hash = true, options if !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(value_code = "#{local_name}")
- "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}"
- 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 value_regexp
- Regexp.new "\\A#{regexp.source}\\Z" if regexp
- end
- 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)
- # All non code-related keys (such as :id, :slug) are URI-unescaped as
- # path parameters.
- default_value = default ? default.inspect : nil
- %[
- value = if (m = match[#{next_capture}])
- URI.unescape(m)
- else
- #{default_value}
- end
- params[:#{key}] = value if value
- ]
- end
-
- def optionality_implied?
- [:action, :id].include? key
- end
-
- end
-
- class ControllerSegment < DynamicSegment #:nodoc:
- def regexp_chunk
- possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name }
- "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))"
- end
-
- # Don't URI.escape the controller name since it may contain slashes.
- def interpolation_chunk(value_code = "#{local_name}")
- "\#{#{value_code}.to_s}"
- end
-
- # 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
-
- def match_extraction(next_capture)
- if default
- "params[:#{key}] = match[#{next_capture}] ? match[#{next_capture}].downcase : '#{default}'"
- else
- "params[:#{key}] = match[#{next_capture}].downcase if match[#{next_capture}]"
- end
- end
- end
-
- class PathSegment < DynamicSegment #:nodoc:
- RESERVED_PCHAR = "#{Segment::RESERVED_PCHAR}/"
- UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze
-
- def interpolation_chunk(value_code = "#{local_name}")
- "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::PathSegment::UNSAFE_PCHAR)}"
- end
-
- def default
- ''
- end
-
- def default=(path)
- raise RoutingError, "paths cannot have non-empty default values" unless path.blank?
- end
-
- 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
-
- def optionality_implied?
- true
- end
-
- class Result < ::Array #:nodoc:
- def to_s() join '/' end
- def self.new_escaped(strings)
- new strings.collect {|str| URI.unescape str}
- end
- end
- end
-
- class RouteBuilder #:nodoc:
- attr_accessor :separators, :optional_separators
-
- def initialize
- self.separators = Routing::SEPARATORS
- self.optional_separators = %w( / )
- end
-
- def separator_pattern(inverted = false)
- "[#{'^' if inverted}#{Regexp.escape(separators.join)}]"
- end
-
- 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
-
- # 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 :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
- [segment, $~.post_match]
- end
-
- # 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)
- options = options.dup
-
- if options[:namespace]
- options[:controller] = "#{options[:path_prefix]}/#{options[:controller]}"
- options.delete(:path_prefix)
- options.delete(:name_prefix)
- options.delete(:namespace)
- end
-
- requirements = (options.delete(:requirements) || {}).dup
- defaults = (options.delete(:defaults) || {}).dup
- conditions = (options.delete(:conditions) || {}).dup
-
- 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
-
- [defaults, requirements, conditions]
- 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
-
- segment_named = Proc.new do |key|
- segments.detect { |segment| segment.key == key if segment.respond_to?(:key) }
- 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)
- if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
- raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
- end
- segment.regexp = requirement
- else
- route_requirements[key] = requirement
- end
- end
-
- 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
-
- assign_default_route_options(segments)
- ensure_required_segments(segments)
- route_requirements
- end
-
- # Assign default options, such as 'index' as a default for :action. This
- # method must be run *after* user supplied requirements and defaults have
- # been applied to the segments.
- def assign_default_route_options(segments)
- segments.each do |segment|
- next unless segment.is_a? DynamicSegment
- case segment.key
- when :action
- if segment.regexp.nil? || segment.regexp.match('index').to_s == 'index'
- segment.default ||= 'index'
- segment.is_optional = true
- end
- when :id
- if segment.default.nil? && segment.regexp.nil? || segment.regexp =~ ''
- segment.is_optional = true
- end
- end
- end
- end
-
- # 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
- end
-
- # 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] == ?/
-
- path = "/#{options[:path_prefix].to_s.gsub(/^\//,'')}#{path}" if options[:path_prefix]
-
- 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
-
- if !route.significant_keys.include?(:action) && !route.requirements[:action]
- route.requirements[:action] = "index"
- route.significant_keys << :action
- end
-
- # Routes cannot use the current string interpolation method
- # if there are user-supplied :requirements as the interpolation
- # code won't raise RoutingErrors when generating
- if options.key?(:requirements) || route.requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION
- route.optimise = false
- end
-
- if !route.significant_keys.include?(:controller)
- raise ArgumentError, "Illegal route: the :controller must be specified!"
- end
-
- route
- end
- end
-
- class RouteSet #:nodoc:
- # 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 #:doc:
- def initialize(set) #:nodoc:
- @set = set
- end
-
- # Create an unnamed route with the provided +path+ and +options+. See
- # ActionController::Routing for an introduction to routes.
- def connect(path, options = {})
- @set.add_route(path, options)
- end
-
- # Creates a named route called "root" for matching the root level request.
- def root(options = {})
- named_route("root", '', options)
- end
-
- def named_route(name, path, options = {}) #:nodoc:
- @set.add_named_route(name, path, options)
- end
-
- # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model.
- # Example:
- #
- # map.namespace(:admin) do |admin|
- # admin.resources :products,
- # :has_many => [ :tags, :images, :variants ]
- # end
- #
- # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController.
- # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for
- # Admin::TagsController.
- def namespace(name, options = {}, &block)
- if options[:namespace]
- with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block)
- else
- with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block)
- end
- end
-
- def method_missing(route_name, *args, &proc) #:nodoc:
- super unless args.length >= 1 && proc.nil?
- @set.add_named_route(route_name, *args)
- end
- end
-
- # 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 #:nodoc:
- include Enumerable
- include ActionController::Routing::Optimisation
- attr_reader :routes, :helpers
-
- def initialize
- clear!
- end
-
- def clear!
- @routes = {}
- @helpers = []
-
- @module ||= Module.new
- @module.instance_methods.each do |selector|
- @module.class_eval { remove_method selector }
- end
- end
-
- def add(name, route)
- routes[name.to_sym] = route
- define_named_route_methods(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 reset!
- old_routes = routes.dup
- clear!
- old_routes.each do |name, route|
- add(name, route)
- end
- end
-
- def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false)
- reset! if regenerate
- Array(destinations).each do |dest|
- dest.send! :include, @module
- end
- end
-
- private
- def url_helper_name(name, kind = :url)
- :"#{name}_#{kind}"
- end
-
- def hash_access_name(name, kind = :url)
- :"hash_for_#{name}_#{kind}"
- end
-
- def define_named_route_methods(name, route)
- {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
- hash = route.defaults.merge(:use_route => name).merge(opts)
- define_hash_access route, name, kind, hash
- define_url_helper route, name, kind, hash
- end
- end
-
- def define_hash_access(route, name, kind, options)
- selector = hash_access_name(name, kind)
- @module.module_eval <<-end_eval # We use module_eval to avoid leaks
- def #{selector}(options = nil)
- options ? #{options.inspect}.merge(options) : #{options.inspect}
- end
- protected :#{selector}
- end_eval
- helpers << selector
- end
-
- def define_url_helper(route, name, kind, options)
- selector = url_helper_name(name, kind)
- # The segment keys used for positional paramters
-
- hash_access_method = hash_access_name(name, kind)
-
- # 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)
- #
- # Also allow options hash, so you can do
- #
- # foo_url(bar, baz, bang, :sort_by => 'baz')
- #
- @module.module_eval <<-end_eval # We use module_eval to avoid leaks
- def #{selector}(*args)
- #{generate_optimisation_block(route, kind)}
-
- opts = if args.empty? || Hash === args.first
- args.first || {}
- else
- options = args.extract_options!
- args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)|
- h[k] = v
- h
- end
- options.merge(args)
- end
-
- url_for(#{hash_access_method}(opts))
- end
- protected :#{selector}
- end_eval
- helpers << selector
- end
- end
-
- attr_accessor :routes, :named_routes
-
- def initialize
- self.routes = []
- self.named_routes = NamedRouteCollection.new
- 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
-
- def draw
- clear!
- yield Mapper.new(self)
- install_helpers
- end
-
- def clear!
- routes.clear
- named_routes.clear
- @combined_regexp = nil
- @routes_by_controller = nil
- end
-
- def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false)
- Array(destinations).each { |d| d.module_eval { include Helpers } }
- named_routes.install(destinations, regenerate_code)
- end
-
- def empty?
- routes.empty?
- end
-
- def load!
- Routing.use_controllers! nil # Clear the controller cache so we may discover new ones
- clear!
- load_routes!
- install_helpers
- end
-
- # reload! will always force a reload whereas load checks the timestamp first
- alias reload! load!
-
- def reload
- if @routes_last_modified && defined?(RAILS_ROOT)
- mtime = File.stat("#{RAILS_ROOT}/config/routes.rb").mtime
- # if it hasn't been changed, then just return
- return if mtime == @routes_last_modified
- # if it has changed then record the new time and fall to the load! below
- @routes_last_modified = mtime
- end
- load!
- end
-
- def load_routes!
- if defined?(RAILS_ROOT) && defined?(::ActionController::Routing::Routes) && self == ::ActionController::Routing::Routes
- load File.join("#{RAILS_ROOT}/config/routes.rb")
- @routes_last_modified = File.stat("#{RAILS_ROOT}/config/routes.rb").mtime
- else
- add_route ":controller/:action/:id"
- end
- end
-
- def add_route(path, options = {})
- route = builder.build(path, options)
- routes << route
- route
- end
-
- def add_named_route(name, path, options = {})
- # TODO - is options EVER used?
- name = options[:name_prefix] + name.to_s if options[:name_prefix]
- named_routes[name.to_sym] = 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?
-
- options_as_params = options.clone
- options_as_params[:action] ||= 'index' if options[:controller]
- options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action]
- options_as_params
- end
-
- def build_expiry(options, recall)
- recall.inject({}) do |expiry, (key, recalled_value)|
- expiry[key] = (options.key?(key) && options[key].to_param != recalled_value.to_param)
- expiry
- end
- end
-
- # 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)
- named_route_name = options.delete(:use_route)
- generate_all = options.delete(:generate_all)
- if named_route_name
- named_route = named_routes[named_route_name]
- options = named_route.parameter_shell.merge(options)
- end
-
- options = options_as_params(options)
- expire_on = build_expiry(options, recall)
-
- if options[:controller]
- options[:controller] = options[:controller].to_s
- end
- # 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 !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/
- old_parts = recall[:controller].split('/')
- new_parts = options[:controller].split('/')
- parts = old_parts[0..-(new_parts.length + 1)] + new_parts
- options[:controller] = parts.join('/')
- end
-
- # 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
- path = named_route.generate(options, merged, expire_on)
- if path.nil?
- raise_named_route_error(options, named_route, named_route_name)
- else
- return path
- end
- else
- merged[:action] ||= 'index'
- options[:action] ||= 'index'
-
- controller = merged[:controller]
- action = merged[:action]
-
- raise RoutingError, "Need controller and action!" unless controller && action
-
- if generate_all
- # Used by caching to expire all paths for a resource
- return routes.collect do |route|
- route.send!(method, options, merged, expire_on)
- end.compact
- end
-
- # don't use the recalled keys when determining which routes to check
- routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }]
-
- routes.each do |route|
- results = route.send!(method, options, merged, expire_on)
- return results if results && (!results.is_a?(Array) || results.first)
- end
- end
-
- raise RoutingError, "No route matches #{options.inspect}"
- end
-
- # try to give a helpful error message when named route generation fails
- def raise_named_route_error(options, named_route, named_route_name)
- diff = named_route.requirements.diff(options)
- unless diff.empty?
- raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff(options).inspect}"
- else
- required_segments = named_route.segments.select {|seg| (!seg.optional?) && (!seg.is_a?(DividerSegment)) }
- required_keys_or_values = required_segments.map { |seg| seg.key rescue seg.value } # we want either the key or the value from the segment
- raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect} - you may have ambiguous routes, or you may need to supply additional parameters for this route. content_url has the following required parameters: #{required_keys_or_values.inspect} - are they all satisfied?"
- end
- 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
-
- allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, :method => verb) } }
-
- if environment[:method] && !HTTP_METHODS.include?(environment[:method])
- raise NotImplemented.new(*allows)
- elsif !allows.empty?
- raise MethodNotAllowed.new(*allows)
- else
- raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}"
- end
- 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
diff --git a/actionpack/lib/action_controller/routing/builder.rb b/actionpack/lib/action_controller/routing/builder.rb
new file mode 100644
index 0000000000..69943d4230
--- /dev/null
+++ b/actionpack/lib/action_controller/routing/builder.rb
@@ -0,0 +1,197 @@
+module ActionController
+ module Routing
+ class RouteBuilder #:nodoc:
+ attr_accessor :separators, :optional_separators
+
+ def initialize
+ self.separators = Routing::SEPARATORS
+ self.optional_separators = %w( / )
+ end
+
+ def separator_pattern(inverted = false)
+ "[#{'^' if inverted}#{Regexp.escape(separators.join)}]"
+ end
+
+ 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
+
+ # 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 :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
+ [segment, $~.post_match]
+ end
+
+ # 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)
+ options = options.dup
+
+ if options[:namespace]
+ options[:controller] = "#{options[:path_prefix]}/#{options[:controller]}"
+ options.delete(:path_prefix)
+ options.delete(:name_prefix)
+ options.delete(:namespace)
+ end
+
+ requirements = (options.delete(:requirements) || {}).dup
+ defaults = (options.delete(:defaults) || {}).dup
+ conditions = (options.delete(:conditions) || {}).dup
+
+ 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
+
+ [defaults, requirements, conditions]
+ 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
+
+ segment_named = Proc.new do |key|
+ segments.detect { |segment| segment.key == key if segment.respond_to?(:key) }
+ 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)
+ if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
+ raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
+ end
+ segment.regexp = requirement
+ else
+ route_requirements[key] = requirement
+ end
+ end
+
+ 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
+
+ assign_default_route_options(segments)
+ ensure_required_segments(segments)
+ route_requirements
+ end
+
+ # Assign default options, such as 'index' as a default for :action. This
+ # method must be run *after* user supplied requirements and defaults have
+ # been applied to the segments.
+ def assign_default_route_options(segments)
+ segments.each do |segment|
+ next unless segment.is_a? DynamicSegment
+ case segment.key
+ when :action
+ if segment.regexp.nil? || segment.regexp.match('index').to_s == 'index'
+ segment.default ||= 'index'
+ segment.is_optional = true
+ end
+ when :id
+ if segment.default.nil? && segment.regexp.nil? || segment.regexp =~ ''
+ segment.is_optional = true
+ end
+ end
+ end
+ end
+
+ # 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
+ end
+
+ # 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] == ?/
+
+ path = "/#{options[:path_prefix].to_s.gsub(/^\//,'')}#{path}" if options[:path_prefix]
+
+ 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
+
+ if !route.significant_keys.include?(:action) && !route.requirements[:action]
+ route.requirements[:action] = "index"
+ route.significant_keys << :action
+ end
+
+ # Routes cannot use the current string interpolation method
+ # if there are user-supplied :requirements as the interpolation
+ # code won't raise RoutingErrors when generating
+ if options.key?(:requirements) || route.requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION
+ route.optimise = false
+ end
+
+ if !route.significant_keys.include?(:controller)
+ raise ArgumentError, "Illegal route: the :controller must be specified!"
+ end
+
+ route
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/routing_optimisation.rb b/actionpack/lib/action_controller/routing/optimisations.rb
index ba4aeb4e82..ba4aeb4e82 100644
--- a/actionpack/lib/action_controller/routing_optimisation.rb
+++ b/actionpack/lib/action_controller/routing/optimisations.rb
diff --git a/actionpack/lib/action_controller/routing/route.rb b/actionpack/lib/action_controller/routing/route.rb
new file mode 100644
index 0000000000..94134a284e
--- /dev/null
+++ b/actionpack/lib/action_controller/routing/route.rb
@@ -0,0 +1,240 @@
+module ActionController
+ module Routing
+ class Route #:nodoc:
+ attr_accessor :segments, :requirements, :conditions, :optimise
+
+ def initialize
+ @segments = []
+ @requirements = {}
+ @conditions = {}
+ @optimise = true
+ end
+
+ # Indicates whether the routes should be optimised with the string interpolation
+ # version of the named routes methods.
+ def optimise?
+ @optimise && ActionController::Base::optimise_named_routes
+ end
+
+ def segment_keys
+ segments.collect do |segment|
+ segment.key if segment.respond_to? :key
+ end.compact
+ end
+
+ # Write and compile a +generate+ method for this Route.
+ def write_generation
+ # Build the main body of the generation
+ body = "expired = false\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.
+ raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
+ instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
+
+ # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
+ # are the same as the keys that were recalled from the previous request. Thus,
+ # we can use the expire_on.keys to determine which keys ought to be used to build
+ # the query string. (Never use keys from the recalled request when building the
+ # query string.)
+
+ method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend"
+ instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
+
+ method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend"
+ instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
+ raw_method
+ 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[:#{key}] == #{req.inspect}"
+ end
+ end
+ requirement_conditions * ' && ' unless requirement_conditions.empty?
+ end
+
+ def generation_structure
+ segments.last.string_structure segments[0..-2]
+ end
+
+ # 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"
+ instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
+ method_decl
+ end
+
+ # 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
+
+ # 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
+
+ # 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
+
+ # Write the real generation implementation and then resend the message.
+ def generate(options, hash, expire_on = {})
+ write_generation
+ generate options, hash, expire_on
+ end
+
+ def generate_extras(options, hash, expire_on = {})
+ write_generation
+ generate_extras options, hash, expire_on
+ end
+
+ # 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, query_keys=nil)
+ return nil unless path
+ query_keys ||= extra_keys(hash)
+ "#{path}#{build_query_string(hash, query_keys)}"
+ end
+
+ # Determine which keys in the given hash are "extra". Extra keys are
+ # those that were not used to generate a particular route. The extra
+ # keys also do not include those recalled from the prior request, nor
+ # do they include any keys that were implied in the route (like a
+ # :controller that is required, but not explicitly used in the text of
+ # the route.)
+ def extra_keys(hash, recall={})
+ (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys
+ end
+
+ # 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 = []
+
+ (only_keys || hash.keys).each do |key|
+ if value = hash[key]
+ elements << value.to_query(key)
+ end
+ end
+
+ elements.empty? ? '' : "?#{elements.sort * '&'}"
+ 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
+
+ # 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 defined? @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 to_s
+ @to_s ||= begin
+ segs = segments.inject("") { |str,s| str << s.to_s }
+ "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect]
+ end
+ 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
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/routing/route_set.rb b/actionpack/lib/action_controller/routing/route_set.rb
new file mode 100644
index 0000000000..83903aff85
--- /dev/null
+++ b/actionpack/lib/action_controller/routing/route_set.rb
@@ -0,0 +1,439 @@
+module ActionController
+ module Routing
+ class RouteSet #:nodoc:
+ # 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 #:doc:
+ def initialize(set) #:nodoc:
+ @set = set
+ end
+
+ # Create an unnamed route with the provided +path+ and +options+. See
+ # ActionController::Routing for an introduction to routes.
+ def connect(path, options = {})
+ @set.add_route(path, options)
+ end
+
+ # Creates a named route called "root" for matching the root level request.
+ def root(options = {})
+ named_route("root", '', options)
+ end
+
+ def named_route(name, path, options = {}) #:nodoc:
+ @set.add_named_route(name, path, options)
+ end
+
+ # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model.
+ # Example:
+ #
+ # map.namespace(:admin) do |admin|
+ # admin.resources :products,
+ # :has_many => [ :tags, :images, :variants ]
+ # end
+ #
+ # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController.
+ # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for
+ # Admin::TagsController.
+ def namespace(name, options = {}, &block)
+ if options[:namespace]
+ with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block)
+ else
+ with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block)
+ end
+ end
+
+ def method_missing(route_name, *args, &proc) #:nodoc:
+ super unless args.length >= 1 && proc.nil?
+ @set.add_named_route(route_name, *args)
+ end
+ end
+
+ # 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 #:nodoc:
+ include Enumerable
+ include ActionController::Routing::Optimisation
+ attr_reader :routes, :helpers
+
+ def initialize
+ clear!
+ end
+
+ def clear!
+ @routes = {}
+ @helpers = []
+
+ @module ||= Module.new
+ @module.instance_methods.each do |selector|
+ @module.class_eval { remove_method selector }
+ end
+ end
+
+ def add(name, route)
+ routes[name.to_sym] = route
+ define_named_route_methods(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 reset!
+ old_routes = routes.dup
+ clear!
+ old_routes.each do |name, route|
+ add(name, route)
+ end
+ end
+
+ def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false)
+ reset! if regenerate
+ Array(destinations).each do |dest|
+ dest.send! :include, @module
+ end
+ end
+
+ private
+ def url_helper_name(name, kind = :url)
+ :"#{name}_#{kind}"
+ end
+
+ def hash_access_name(name, kind = :url)
+ :"hash_for_#{name}_#{kind}"
+ end
+
+ def define_named_route_methods(name, route)
+ {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
+ hash = route.defaults.merge(:use_route => name).merge(opts)
+ define_hash_access route, name, kind, hash
+ define_url_helper route, name, kind, hash
+ end
+ end
+
+ def define_hash_access(route, name, kind, options)
+ selector = hash_access_name(name, kind)
+ @module.module_eval <<-end_eval # We use module_eval to avoid leaks
+ def #{selector}(options = nil)
+ options ? #{options.inspect}.merge(options) : #{options.inspect}
+ end
+ protected :#{selector}
+ end_eval
+ helpers << selector
+ end
+
+ def define_url_helper(route, name, kind, options)
+ selector = url_helper_name(name, kind)
+ # The segment keys used for positional paramters
+
+ hash_access_method = hash_access_name(name, kind)
+
+ # 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)
+ #
+ # Also allow options hash, so you can do
+ #
+ # foo_url(bar, baz, bang, :sort_by => 'baz')
+ #
+ @module.module_eval <<-end_eval # We use module_eval to avoid leaks
+ def #{selector}(*args)
+ #{generate_optimisation_block(route, kind)}
+
+ opts = if args.empty? || Hash === args.first
+ args.first || {}
+ else
+ options = args.extract_options!
+ args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)|
+ h[k] = v
+ h
+ end
+ options.merge(args)
+ end
+
+ url_for(#{hash_access_method}(opts))
+ end
+ protected :#{selector}
+ end_eval
+ helpers << selector
+ end
+ end
+
+ attr_accessor :routes, :named_routes
+
+ def initialize
+ self.routes = []
+ self.named_routes = NamedRouteCollection.new
+ 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
+
+ def draw
+ clear!
+ yield Mapper.new(self)
+ install_helpers
+ end
+
+ def clear!
+ routes.clear
+ named_routes.clear
+ @combined_regexp = nil
+ @routes_by_controller = nil
+ end
+
+ def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false)
+ Array(destinations).each { |d| d.module_eval { include Helpers } }
+ named_routes.install(destinations, regenerate_code)
+ end
+
+ def empty?
+ routes.empty?
+ end
+
+ def load!
+ Routing.use_controllers! nil # Clear the controller cache so we may discover new ones
+ clear!
+ load_routes!
+ install_helpers
+ end
+
+ # reload! will always force a reload whereas load checks the timestamp first
+ alias reload! load!
+
+ def reload
+ if @routes_last_modified && defined?(RAILS_ROOT)
+ mtime = File.stat("#{RAILS_ROOT}/config/routes.rb").mtime
+ # if it hasn't been changed, then just return
+ return if mtime == @routes_last_modified
+ # if it has changed then record the new time and fall to the load! below
+ @routes_last_modified = mtime
+ end
+ load!
+ end
+
+ def load_routes!
+ if defined?(RAILS_ROOT) && defined?(::ActionController::Routing::Routes) && self == ::ActionController::Routing::Routes
+ load File.join("#{RAILS_ROOT}/config/routes.rb")
+ @routes_last_modified = File.stat("#{RAILS_ROOT}/config/routes.rb").mtime
+ else
+ add_route ":controller/:action/:id"
+ end
+ end
+
+ def add_route(path, options = {})
+ route = builder.build(path, options)
+ routes << route
+ route
+ end
+
+ def add_named_route(name, path, options = {})
+ # TODO - is options EVER used?
+ name = options[:name_prefix] + name.to_s if options[:name_prefix]
+ named_routes[name.to_sym] = 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?
+
+ options_as_params = options.clone
+ options_as_params[:action] ||= 'index' if options[:controller]
+ options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action]
+ options_as_params
+ end
+
+ def build_expiry(options, recall)
+ recall.inject({}) do |expiry, (key, recalled_value)|
+ expiry[key] = (options.key?(key) && options[key].to_param != recalled_value.to_param)
+ expiry
+ end
+ end
+
+ # 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)
+ named_route_name = options.delete(:use_route)
+ generate_all = options.delete(:generate_all)
+ if named_route_name
+ named_route = named_routes[named_route_name]
+ options = named_route.parameter_shell.merge(options)
+ end
+
+ options = options_as_params(options)
+ expire_on = build_expiry(options, recall)
+
+ if options[:controller]
+ options[:controller] = options[:controller].to_s
+ end
+ # 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 !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/
+ old_parts = recall[:controller].split('/')
+ new_parts = options[:controller].split('/')
+ parts = old_parts[0..-(new_parts.length + 1)] + new_parts
+ options[:controller] = parts.join('/')
+ end
+
+ # 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
+ path = named_route.generate(options, merged, expire_on)
+ if path.nil?
+ raise_named_route_error(options, named_route, named_route_name)
+ else
+ return path
+ end
+ else
+ merged[:action] ||= 'index'
+ options[:action] ||= 'index'
+
+ controller = merged[:controller]
+ action = merged[:action]
+
+ raise RoutingError, "Need controller and action!" unless controller && action
+
+ if generate_all
+ # Used by caching to expire all paths for a resource
+ return routes.collect do |route|
+ route.send!(method, options, merged, expire_on)
+ end.compact
+ end
+
+ # don't use the recalled keys when determining which routes to check
+ routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }]
+
+ routes.each do |route|
+ results = route.send!(method, options, merged, expire_on)
+ return results if results && (!results.is_a?(Array) || results.first)
+ end
+ end
+
+ raise RoutingError, "No route matches #{options.inspect}"
+ end
+
+ # try to give a helpful error message when named route generation fails
+ def raise_named_route_error(options, named_route, named_route_name)
+ diff = named_route.requirements.diff(options)
+ unless diff.empty?
+ raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff(options).inspect}"
+ else
+ required_segments = named_route.segments.select {|seg| (!seg.optional?) && (!seg.is_a?(DividerSegment)) }
+ required_keys_or_values = required_segments.map { |seg| seg.key rescue seg.value } # we want either the key or the value from the segment
+ raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect} - you may have ambiguous routes, or you may need to supply additional parameters for this route. content_url has the following required parameters: #{required_keys_or_values.inspect} - are they all satisfied?"
+ end
+ 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
+
+ allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, :method => verb) } }
+
+ if environment[:method] && !HTTP_METHODS.include?(environment[:method])
+ raise NotImplemented.new(*allows)
+ elsif !allows.empty?
+ raise MethodNotAllowed.new(*allows)
+ else
+ raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}"
+ end
+ 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
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/routing/routing_ext.rb b/actionpack/lib/action_controller/routing/routing_ext.rb
new file mode 100644
index 0000000000..2ad20ee699
--- /dev/null
+++ b/actionpack/lib/action_controller/routing/routing_ext.rb
@@ -0,0 +1,46 @@
+
+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 #:nodoc:
+ 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
diff --git a/actionpack/lib/action_controller/routing/segments.rb b/actionpack/lib/action_controller/routing/segments.rb
new file mode 100644
index 0000000000..efac2df1cb
--- /dev/null
+++ b/actionpack/lib/action_controller/routing/segments.rb
@@ -0,0 +1,268 @@
+module ActionController
+ module Routing
+ class Segment #:nodoc:
+ RESERVED_PCHAR = ':@&=+$,;'
+ UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze
+
+ attr_accessor :is_optional
+ alias_method :optional?, :is_optional
+
+ def initialize
+ self.is_optional = false
+ end
+
+ 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 interpolation_chunk
+ URI.escape(value, UNSAFE_PCHAR)
+ end
+
+ # 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 StaticSegment < Segment #:nodoc:
+ attr_accessor :value, :raw
+ alias_method :raw?, :raw
+
+ def initialize(value = nil)
+ super()
+ self.value = value
+ end
+
+ def interpolation_chunk
+ raw? ? value : super
+ end
+
+ 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
+ escaped + pattern
+ end
+ end
+
+ def to_s
+ value
+ end
+ end
+
+ class DividerSegment < StaticSegment #:nodoc:
+ def initialize(value = nil)
+ super(value)
+ self.raw = true
+ self.is_optional = true
+ end
+
+ def optionality_implied?
+ true
+ end
+ end
+
+ class DynamicSegment < Segment #:nodoc:
+ 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 to_s
+ ":#{key}"
+ end
+
+ # 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}] && hash[:#{key}].to_param #{"|| #{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
+ end
+ def expiry_statement
+ "expired, hash = true, options if !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(value_code = "#{local_name}")
+ "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}"
+ 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 value_regexp
+ Regexp.new "\\A#{regexp.source}\\Z" if regexp
+ end
+ 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)
+ # All non code-related keys (such as :id, :slug) are URI-unescaped as
+ # path parameters.
+ default_value = default ? default.inspect : nil
+ %[
+ value = if (m = match[#{next_capture}])
+ URI.unescape(m)
+ else
+ #{default_value}
+ end
+ params[:#{key}] = value if value
+ ]
+ end
+
+ def optionality_implied?
+ [:action, :id].include? key
+ end
+
+ end
+
+ class ControllerSegment < DynamicSegment #:nodoc:
+ def regexp_chunk
+ possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name }
+ "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))"
+ end
+
+ # Don't URI.escape the controller name since it may contain slashes.
+ def interpolation_chunk(value_code = "#{local_name}")
+ "\#{#{value_code}.to_s}"
+ end
+
+ # 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
+
+ def match_extraction(next_capture)
+ if default
+ "params[:#{key}] = match[#{next_capture}] ? match[#{next_capture}].downcase : '#{default}'"
+ else
+ "params[:#{key}] = match[#{next_capture}].downcase if match[#{next_capture}]"
+ end
+ end
+ end
+
+ class PathSegment < DynamicSegment #:nodoc:
+ RESERVED_PCHAR = "#{Segment::RESERVED_PCHAR}/"
+ UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze
+
+ def interpolation_chunk(value_code = "#{local_name}")
+ "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::PathSegment::UNSAFE_PCHAR)}"
+ end
+
+ def default
+ ''
+ end
+
+ def default=(path)
+ raise RoutingError, "paths cannot have non-empty default values" unless path.blank?
+ end
+
+ 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
+
+ def optionality_implied?
+ true
+ end
+
+ class Result < ::Array #:nodoc:
+ def to_s() join '/' end
+ def self.new_escaped(strings)
+ new strings.collect {|str| URI.unescape str}
+ end
+ end
+ end
+ end
+end