diff options
Diffstat (limited to 'actionpack')
14 files changed, 341 insertions, 1264 deletions
diff --git a/actionpack/Gemfile b/actionpack/Gemfile index b3ffeaecb6..af95766608 100644 --- a/actionpack/Gemfile +++ b/actionpack/Gemfile @@ -3,6 +3,7 @@ rails_root = Pathname.new(File.dirname(__FILE__)).join("..") Gem.sources.each { |uri| source uri } gem "rack", "1.0.1", :git => "git://github.com/rails/rack.git", :branch => "rack-1.0" +gem "rack-mount", :git => "git://github.com/josh/rack-mount.git" gem "rack-test", "~> 0.5.0" gem "activesupport", "3.0.pre", :vendored_at => rails_root.join("activesupport") gem "activemodel", "3.0.pre", :vendored_at => rails_root.join("activemodel") diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 6702cb47f8..809e5f2ad4 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -26,14 +26,14 @@ module ActionController autoload :IntegrationTest, 'action_controller/deprecated/integration_test' autoload :MimeResponds, 'action_controller/metal/mime_responds' autoload :PerformanceTest, 'action_controller/deprecated/performance_test' - autoload :PolymorphicRoutes, 'action_controller/routing/generation/polymorphic_routes' + autoload :PolymorphicRoutes, 'action_controller/routing/polymorphic_routes' autoload :RecordIdentifier, 'action_controller/record_identifier' autoload :Resources, 'action_controller/routing/resources' autoload :SessionManagement, 'action_controller/metal/session_management' autoload :TestCase, 'action_controller/testing/test_case' autoload :TestProcess, 'action_controller/testing/process' - autoload :UrlRewriter, 'action_controller/routing/generation/url_rewriter' - autoload :UrlWriter, 'action_controller/routing/generation/url_rewriter' + autoload :UrlRewriter, 'action_controller/routing/url_rewriter' + autoload :UrlWriter, 'action_controller/routing/url_rewriter' autoload :Verification, 'action_controller/metal/verification' autoload :Flash, 'action_controller/metal/flash' diff --git a/actionpack/lib/action_controller/routing.rb b/actionpack/lib/action_controller/routing.rb index 5b9ded83dd..979a4ad8c9 100644 --- a/actionpack/lib/action_controller/routing.rb +++ b/actionpack/lib/action_controller/routing.rb @@ -1,16 +1,8 @@ -require 'cgi' -require 'uri' -require 'set' - -require 'active_support/core_ext/module/aliasing' -require 'active_support/core_ext/module/attribute_accessors' -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 'active_support/core_ext/object/conversions' +require 'active_support/core_ext/boolean/conversions' +require 'active_support/core_ext/nil/conversions' +require 'active_support/core_ext/regexp' require 'action_controller/routing/route_set' -require 'action_controller/routing/recognition_optimisation' module ActionController # == Routing @@ -197,7 +189,7 @@ module ActionController # # map.connect '*path' , :controller => 'blog' , :action => 'unrecognized?' # - # will glob all remaining parts of the route that were not recognized earlier. + # will glob all remaining parts of the route that were not recognized earlier. # The globbed values are in <tt>params[:path]</tt> as an array of path segments. # # == Route conditions diff --git a/actionpack/lib/action_controller/routing/builder.rb b/actionpack/lib/action_controller/routing/builder.rb deleted file mode 100644 index 42ad12e1ea..0000000000 --- a/actionpack/lib/action_controller/routing/builder.rb +++ /dev/null @@ -1,199 +0,0 @@ -require 'active_support/core_ext/hash/except' - -module ActionController - module Routing - class RouteBuilder #:nodoc: - attr_reader :separators, :optional_separators - attr_reader :separator_regexp, :nonseparator_regexp, :interval_regexp - - def initialize - @separators = Routing::SEPARATORS - @optional_separators = %w( / ) - - @separator_regexp = /[#{Regexp.escape(separators.join)}]/ - @nonseparator_regexp = /\A([^#{Regexp.escape(separators.join)}]+)/ - @interval_regexp = /(.*?)(#{separator_regexp}|$)/ - 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 - # <tt>optional?</tt> 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\.(:format)?\// - OptionalFormatSegment.new - when /\A:(\w+)/ - key = $1.to_sym - key == :controller ? ControllerSegment.new(key) : DynamicSegment.new(key) - when /\A\*(\w+)/ - PathSegment.new($1.to_sym, :optional => true) - when /\A\?(.*?)\?/ - StaticSegment.new($1, :optional => true) - when nonseparator_regexp - StaticSegment.new($1) - when separator_regexp - DividerSegment.new($&, :optional => optional_separators.include?($&)) - 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.except(:path_prefix, :name_prefix) - - if options[:namespace] - options[:controller] = "#{options.delete(:namespace).sub(/\/$/, '')}/#{options[:controller]}" - end - - requirements = (options.delete(:requirements) || {}).dup - defaults = (options.delete(:defaults) || {}).dup - conditions = (options.delete(:conditions) || {}).dup - - validate_route_conditions(conditions) - - 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 - if requirement.multiline? - raise ArgumentError, "Regexp multiline option 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 <tt>:action</tt>. 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] == ?/ - - prefix = options[:path_prefix].to_s.gsub(/^\//,'') - path = "/#{prefix}#{path}" unless prefix.blank? - - segments = segments_for_route_path(path) - defaults, requirements, conditions = divide_route_options(segments, options) - requirements = assign_route_options(segments, defaults, requirements) - - # TODO: Segments should be frozen on initialize - segments.each { |segment| segment.freeze } - - route = Route.new(segments, requirements, conditions) - - if !route.significant_keys.include?(:controller) - raise ArgumentError, "Illegal route: the :controller must be specified!" - end - - route.freeze - end - - private - def validate_route_conditions(conditions) - if method = conditions[:method] - [method].flatten.each do |m| - if m == :head - raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers" - end - - unless HTTP_METHODS.include?(m.to_sym) - raise ArgumentError, "Invalid HTTP method specified in route conditions: #{conditions.inspect}" - end - end - end - end - end - end -end diff --git a/actionpack/lib/action_controller/routing/optimisations.rb b/actionpack/lib/action_controller/routing/optimisations.rb deleted file mode 100644 index 714cf97861..0000000000 --- a/actionpack/lib/action_controller/routing/optimisations.rb +++ /dev/null @@ -1,130 +0,0 @@ -module ActionController - module Routing - # Much of the slow performance from routes comes from the - # complexity of expiry, <tt>:requirements</tt> matching, defaults providing - # and figuring out which url pattern to use. With named routes - # we can avoid the expense of finding the right route. So if - # they've provided the right number of arguments, and have no - # <tt>:requirements</tt>, we can just build up a string and return it. - # - # To support building optimisations for other common cases, the - # generation code is separated into several classes - module Optimisation - def generate_optimisation_block(route, kind) - return "" unless route.optimise? - OPTIMISERS.inject("") do |memo, klazz| - memo << klazz.new(route, kind).source_code - memo - end - end - - class Optimiser - attr_reader :route, :kind - GLOBAL_GUARD_CONDITIONS = [ - "(!defined?(default_url_options) || default_url_options.blank?)", - "(!defined?(controller.default_url_options) || controller.default_url_options.blank?)", - "defined?(request)", - "request" - ] - - def initialize(route, kind) - @route = route - @kind = kind - end - - def guard_conditions - ["false"] - end - - def generation_code - 'nil' - end - - def source_code - if applicable? - guard_condition = (GLOBAL_GUARD_CONDITIONS + guard_conditions).join(" && ") - "return #{generation_code} if #{guard_condition}\n" - else - "\n" - end - end - - # Temporarily disabled <tt>:url</tt> optimisation pending proper solution to - # Issues around request.host etc. - def applicable? - true - end - end - - # Given a route - # - # map.person '/people/:id' - # - # If the user calls <tt>person_url(@person)</tt>, we can simply - # return a string like "/people/#{@person.to_param}" - # rather than triggering the expensive logic in +url_for+. - class PositionalArguments < Optimiser - def guard_conditions - number_of_arguments = route.required_segment_keys.size - # if they're using foo_url(:id=>2) it's one - # argument, but we don't want to generate /foos/id2 - if number_of_arguments == 1 - ["args.size == 1", "!args.first.is_a?(Hash)"] - else - ["args.size == #{number_of_arguments}"] - end - end - - def generation_code - elements = [] - idx = 0 - - if kind == :url - elements << '#{request.protocol}' - elements << '#{request.host_with_port}' - end - - elements << '#{ActionController::Base.relative_url_root if ActionController::Base.relative_url_root}' - - # The last entry in <tt>route.segments</tt> appears to *always* be a - # 'divider segment' for '/' but we have assertions to ensure that - # we don't include the trailing slashes, so skip them. - (route.segments.size == 1 ? route.segments : route.segments[0..-2]).each do |segment| - if segment.is_a?(DynamicSegment) - elements << segment.interpolation_chunk("args[#{idx}].to_param") - idx += 1 - else - elements << segment.interpolation_chunk - end - end - %("#{elements * ''}") - end - end - - # This case is mostly the same as the positional arguments case - # above, but it supports additional query parameters as the last - # argument - class PositionalArgumentsWithAdditionalParams < PositionalArguments - def guard_conditions - ["args.size == #{route.segment_keys.size + 1}"] + - UrlRewriter::RESERVED_OPTIONS.collect{ |key| "!args.last.has_key?(:#{key})" } - end - - # This case uses almost the same code as positional arguments, - # but add a question mark and args.last.to_query on the end, - # unless the last arg is empty - def generation_code - super.insert(-2, '#{\'?\' + args.last.to_query unless args.last.empty?}') - end - - # To avoid generating "http://localhost/?host=foo.example.com" we - # can't use this optimisation on routes without any segments - def applicable? - super && route.segment_keys.size > 0 - end - end - - OPTIMISERS = [PositionalArguments, PositionalArgumentsWithAdditionalParams] - end - end -end diff --git a/actionpack/lib/action_controller/routing/generation/polymorphic_routes.rb b/actionpack/lib/action_controller/routing/polymorphic_routes.rb index 2adf3575a7..2adf3575a7 100644 --- a/actionpack/lib/action_controller/routing/generation/polymorphic_routes.rb +++ b/actionpack/lib/action_controller/routing/polymorphic_routes.rb diff --git a/actionpack/lib/action_controller/routing/recognition_optimisation.rb b/actionpack/lib/action_controller/routing/recognition_optimisation.rb deleted file mode 100644 index 9bfebff0c0..0000000000 --- a/actionpack/lib/action_controller/routing/recognition_optimisation.rb +++ /dev/null @@ -1,167 +0,0 @@ -module ActionController - module Routing - # BEFORE: 0.191446860631307 ms/url - # AFTER: 0.029847304022858 ms/url - # Speed up: 6.4 times - # - # Route recognition is slow due to one-by-one iterating over - # a whole routeset (each map.resources generates at least 14 routes) - # and matching weird regexps on each step. - # - # We optimize this by skipping all URI segments that 100% sure can't - # be matched, moving deeper in a tree of routes (where node == segment) - # until first possible match is accured. In such case, we start walking - # a flat list of routes, matching them with accurate matcher. - # So, first step: search a segment tree for the first relevant index. - # Second step: iterate routes starting with that index. - # - # How tree is walked? We can do a recursive tests, but it's smarter: - # We just create a tree of if-s and elsif-s matching segments. - # - # We have segments of 3 flavors: - # 1) nil (no segment, route finished) - # 2) const-dot-dynamic (like "/posts.:xml", "/preview.:size.jpg") - # 3) const (like "/posts", "/comments") - # 4) dynamic ("/:id", "file.:size.:extension") - # - # We split incoming string into segments and iterate over them. - # When segment is nil, we drop immediately, on a current node index. - # When segment is equal to some const, we step into branch. - # If none constants matched, we step into 'dynamic' branch (it's a last). - # If we can't match anything, we drop to last index on a level. - # - # Note: we maintain the original routes order, so we finish building - # steps on a first dynamic segment. - # - # - # Example. Given the routes: - # 0 /posts/ - # 1 /posts/:id - # 2 /posts/:id/comments - # 3 /posts/blah - # 4 /users/ - # 5 /users/:id - # 6 /users/:id/profile - # - # request_uri = /users/123 - # - # There will be only 4 iterations: - # 1) segm test for /posts prefix, skip all /posts/* routes - # 2) segm test for /users/ - # 3) segm test for /users/:id - # (jump to list index = 5) - # 4) full test for /users/:id => here we are! - class RouteSet - def recognize_path(path, environment={}) - result = recognize_optimized(path, environment) and return result - - # Route was not recognized. Try to find out why (maybe wrong verb). - allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, environment.merge(: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 segment_tree(routes) - tree = [0] - - i = -1 - routes.each do |route| - i += 1 - # not fast, but runs only once - segments = to_plain_segments(route.segments.inject("") { |str,s| str << s.to_s }) - - node = tree - segments.each do |seg| - seg = :dynamic if seg && seg[0] == ?: - node << [seg, [i]] if node.empty? || node[node.size - 1][0] != seg - node = node[node.size - 1][1] - end - end - tree - end - - def generate_code(list, padding=' ', level = 0) - # a digit - return padding + "#{list[0]}\n" if list.size == 1 && !(Array === list[0]) - - body = padding + "(seg = segments[#{level}]; \n" - - i = 0 - was_nil = false - list.each do |item| - if Array === item - i += 1 - start = (i == 1) - tag, sub = item - if tag == :dynamic - body += padding + "#{start ? 'if' : 'elsif'} true\n" - body += generate_code(sub, padding + " ", level + 1) - break - elsif tag == nil && !was_nil - was_nil = true - body += padding + "#{start ? 'if' : 'elsif'} seg.nil?\n" - body += generate_code(sub, padding + " ", level + 1) - else - body += padding + "#{start ? 'if' : 'elsif'} seg == '#{tag}'\n" - body += generate_code(sub, padding + " ", level + 1) - end - end - end - body += padding + "else\n" - body += padding + " #{list[0]}\n" - body += padding + "end)\n" - body - end - - # this must be really fast - def to_plain_segments(str) - str = str.dup - str.sub!(/^\/+/,'') - str.sub!(/\/+$/,'') - segments = str.split(/\.[^\/]+\/+|\/+|\.[^\/]+\Z/) # cut off ".format" also - segments << nil - segments - end - - private - def write_recognize_optimized! - tree = segment_tree(routes) - body = generate_code(tree) - - remove_recognize_optimized! - - instance_eval %{ - def recognize_optimized(path, env) - segments = to_plain_segments(path) - index = #{body} - return nil unless index - while index < routes.size - result = routes[index].recognize(path, env) and return result - index += 1 - end - nil - end - }, '(recognize_optimized)', 1 - end - - def clear_recognize_optimized! - remove_recognize_optimized! - write_recognize_optimized! - end - - def remove_recognize_optimized! - if respond_to?(:recognize_optimized) - class << self - remove_method :recognize_optimized - end - end - end - end - end -end diff --git a/actionpack/lib/action_controller/routing/route.rb b/actionpack/lib/action_controller/routing/route.rb deleted file mode 100644 index eba05a3c5a..0000000000 --- a/actionpack/lib/action_controller/routing/route.rb +++ /dev/null @@ -1,267 +0,0 @@ -require 'active_support/core_ext/object/misc' - -module ActionController - module Routing - class Route #:nodoc: - attr_accessor :segments, :requirements, :conditions, :optimise - - def initialize(segments = [], requirements = {}, conditions = {}) - @segments = segments - @requirements = requirements - @conditions = conditions - - if !significant_keys.include?(:action) && !requirements[:action] - @requirements[:action] = "index" - @significant_keys << :action - end - - # Routes cannot use the current string interpolation method - # if there are user-supplied <tt>:requirements</tt> as the interpolation - # code won't raise RoutingErrors when generating - has_requirements = @segments.detect { |segment| segment.respond_to?(:regexp) && segment.regexp } - if has_requirements || @requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION - @optimise = false - else - @optimise = true - end - 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 - - def required_segment_keys - required_segments = segments.select {|seg| (!seg.optional? && !seg.is_a?(DividerSegment)) || seg.is_a?(PathSegment) } - required_segments.collect { |seg| seg.key if seg.respond_to?(:key)}.compact - 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 - - # 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 ||= {}.tap 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 ||= [].tap 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 ||= {}.tap 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) - prepare_matching! - (@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 - - # TODO: Route should be prepared and frozen on initialize - def freeze - unless frozen? - write_generation! - write_recognition! - prepare_matching! - - parameter_shell - significant_keys - defaults - to_s - end - - super - end - - def generate(options, hash, expire_on = {}) - path, hash = generate_raw(options, hash, expire_on) - append_query_string(path, hash, extra_keys(options)) - end - - def generate_extras(options, hash, expire_on = {}) - path, hash = generate_raw(options, hash, expire_on) - [path, extra_keys(options)] - end - - private - 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 - - # 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.) - - 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.to_s}\\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]].flatten.include?(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 += segment.number_of_captures - x - end - extraction.compact - 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 - # <tt>:controller</tt> 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 - - def prepare_matching! - unless defined? @matching_prepared - @controller_requirement = requirement_for(:controller) - @action_requirement = requirement_for(:action) - @matching_prepared = true - end - end - end - end -end diff --git a/actionpack/lib/action_controller/routing/route_set.rb b/actionpack/lib/action_controller/routing/route_set.rb index 25fdbf480e..8135b5811e 100644 --- a/actionpack/lib/action_controller/routing/route_set.rb +++ b/actionpack/lib/action_controller/routing/route_set.rb @@ -1,6 +1,58 @@ +require 'rack/mount' +require 'forwardable' + module ActionController module Routing class RouteSet #:nodoc: + NotFound = lambda { |env| + raise RoutingError, "No route matches #{env[::Rack::Mount::Const::PATH_INFO].inspect} with #{env.inspect}" + } + + PARAMETERS_KEY = 'action_dispatch.request.path_parameters' + + class Dispatcher + def initialize(options = {}) + defaults = options[:defaults] + @glob_param = options.delete(:glob) + end + + def call(env) + params = env[PARAMETERS_KEY] + merge_default_action!(params) + split_glob_param!(params) if @glob_param + params.each { |key, value| params[key] = URI.unescape(value) if value.is_a?(String) } + + if env['action_controller.recognize'] + [200, {}, params] + else + controller = controller(params) + controller.action(params[:action]).call(env) + end + end + + private + def controller(params) + if params && params.has_key?(:controller) + controller = "#{params[:controller].camelize}Controller" + ActiveSupport::Inflector.constantize(controller) + end + end + + def merge_default_action!(params) + params[:action] ||= 'index' + end + + def split_glob_param!(params) + params[@glob_param] = params[@glob_param].split('/').map { |v| URI.unescape(v) } + end + end + + module RouteExtensions + def segment_keys + conditions[:path_info].names.compact.map { |key| key.to_sym } + end + end + # Mapper instances are used to build routes. The object passed to the draw # block in config/routes.rb is a Mapper instance. # @@ -63,7 +115,6 @@ module ActionController # named routes. class NamedRouteCollection #:nodoc: include Enumerable - include ActionController::Routing::Optimisation attr_reader :routes, :helpers def initialize @@ -175,8 +226,6 @@ module ActionController named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks def #{selector}(*args) # def users_url(*args) # - #{generate_optimisation_block(route, kind)} # #{generate_optimisation_block(route, kind)} - # opts = if args.empty? || Hash === args.first # opts = if args.empty? || Hash === args.first args.first || {} # args.first || {} else # else @@ -216,28 +265,18 @@ module ActionController clear! 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) + @set.add_route(NotFound) install_helpers + @set.freeze end def clear! routes.clear named_routes.clear - - @combined_regexp = nil - @routes_by_controller = nil - - # This will force routing/recognition_optimization.rb - # to refresh optimisations. - clear_recognize_optimized! + @set = ::Rack::Mount::RouteSet.new(:parameters_key => PARAMETERS_KEY) end def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) @@ -257,7 +296,7 @@ module ActionController def configuration_file=(path) add_configuration_file(path) end - + # Deprecated accessor def configuration_file configuration_files @@ -296,29 +335,119 @@ module ActionController def routes_changed_at routes_changed_at = nil - + configuration_files.each do |config| config_changed_at = File.stat(config).mtime if routes_changed_at.nil? || config_changed_at > routes_changed_at - routes_changed_at = config_changed_at + routes_changed_at = config_changed_at end end - + routes_changed_at end def add_route(path, options = {}) - options.each { |k, v| options[k] = v.to_s if [:controller, :action].include?(k) && v.is_a?(Symbol) } - route = builder.build(path, options) + options = options.dup + + if conditions = options.delete(:conditions) + conditions = conditions.dup + method = [conditions.delete(:method)].flatten.compact + method.map! { |m| + m = m.to_s.upcase + + if m == "HEAD" + raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers" + end + + unless HTTP_METHODS.include?(m.downcase.to_sym) + raise ArgumentError, "Invalid HTTP method specified in route conditions" + end + + m + } + + if method.length > 1 + method = Regexp.union(*method) + elsif method.length == 1 + method = method.first + else + method = nil + end + end + + path_prefix = options.delete(:path_prefix) + name_prefix = options.delete(:name_prefix) + namespace = options.delete(:namespace) + + name = options.delete(:_name) + name = "#{name_prefix}#{name}" if name_prefix + + requirements = options.delete(:requirements) || {} + defaults = options.delete(:defaults) || {} + options.each do |k, v| + if v.is_a?(Regexp) + if value = options.delete(k) + requirements[k.to_sym] = value + end + else + value = options.delete(k) + defaults[k.to_sym] = value.is_a?(Symbol) ? value : value.to_param + end + end + + requirements.each do |_, requirement| + if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" + end + if requirement.multiline? + raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" + end + end + + possible_names = Routing.possible_controllers.collect { |n| Regexp.escape(n) } + requirements[:controller] ||= Regexp.union(*possible_names) + + if defaults[:controller] + defaults[:action] ||= 'index' + defaults[:controller] = defaults[:controller].to_s + defaults[:controller] = "#{namespace}#{defaults[:controller]}" if namespace + end + + if defaults[:action] + defaults[:action] = defaults[:action].to_s + end + + if path.is_a?(String) + path = "#{path_prefix}/#{path}" if path_prefix + path = path.gsub('.:format', '(.:format)') + path = optionalize_trailing_dynamic_segments(path, requirements, defaults) + glob = $1.to_sym if path =~ /\/\*(\w+)$/ + path = ::Rack::Mount::Utils.normalize_path(path) + path = ::Rack::Mount::Strexp.compile(path, requirements, %w( / . ? )) + + if glob && !defaults[glob].blank? + raise RoutingError, "paths cannot have non-empty default values" + end + end + + app = Dispatcher.new(:defaults => defaults, :glob => glob) + + conditions = {} + conditions[:request_method] = method if method + conditions[:path_info] = path if path + + route = @set.add_route(app, conditions, defaults, name) + route.extend(RouteExtensions) 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) + options[:_name] = name + route = add_route(path, options) + named_routes[route.name] = route + route end def options_as_params(options) @@ -356,24 +485,29 @@ module ActionController 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 + def generate(options, recall = {}, method = :generate) + options, recall = options.dup, recall.dup + named_route = options.delete(:use_route) options = options_as_params(options) expire_on = build_expiry(options, recall) - if options[:controller] - options[:controller] = options[:controller].to_s + recall[:action] ||= 'index' if options[:controller] || recall[:controller] + + if recall[:controller] && (!options.has_key?(:controller) || options[:controller] == recall[:controller]) + options[:controller] = recall.delete(:controller) + + if recall[:action] && (!options.has_key?(:action) || options[:action] == recall[:action]) + options[:action] = recall.delete(:action) + + if recall[:id] && (!options.has_key?(:id) || options[:id] == recall[:id]) + options[:id] = recall.delete(:id) + end + end 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. + + options[:controller] = options[:controller].to_s if options[:controller] + if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/ old_parts = recall[:controller].split('/') new_parts = options[:controller].split('/') @@ -381,98 +515,75 @@ module ActionController 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 + merged = options.merge(recall) + if options.has_key?(:action) && options[:action].nil? + options.delete(:action) + recall[:action] = 'index' + end + recall[:action] = options.delete(:action) if options[:action] == 'index' + + path = _uri(named_route, options, recall) + if path && method == :generate_extras + uri = URI(path) + extras = uri.query ? + Rack::Utils.parse_nested_query(uri.query).keys.map { |k| k.to_sym } : + [] + [uri.path, extras] + elsif path + path 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.reject {|k,v| !v}.keys.sort_by { |x| x.object_id }] - - routes.each_with_index do |route, index| - results = route.__send__(method, options, merged, expire_on) - if results && (!results.is_a?(Array) || results.first) - return results - end - end + raise RoutingError, "No route matches #{options.inspect}" end - + rescue Rack::Mount::RoutingError 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}" + def call(env) + @set.call(env) + rescue ActionController::RoutingError => e + raise e if env['action_controller.rescue_error'] == false + + method, path = env['REQUEST_METHOD'].downcase.to_sym, env['PATH_INFO'] + + # Route was not recognized. Try to find out why (maybe wrong verb). + allows = HTTP_METHODS.select { |verb| + begin + recognize_path(path, {:method => verb}, false) + rescue ActionController::RoutingError + nil + end + } + + if !HTTP_METHODS.include?(method) + raise NotImplemented.new(*allows) + elsif !allows.empty? + raise MethodNotAllowed.new(*allows) 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?" + raise e end end - def call(env) - request = ActionDispatch::Request.new(env) - app = Routing::Routes.recognize(request) - app.action(request.parameters[:action] || 'index').call(env) - end - def recognize(request) params = recognize_path(request.path, extract_request_environment(request)) request.path_parameters = params.with_indifferent_access "#{params[:controller].to_s.camelize}Controller".constantize end - def recognize_path(path, environment={}) - raise "Not optimized! Check that routing/recognition_optimisation overrides RouteSet#recognize_path." - end + def recognize_path(path, environment = {}, rescue_error = true) + method = (environment[:method] || "GET").to_s.upcase - 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 + begin + env = Rack::MockRequest.env_for(path, {:method => method}) + rescue URI::InvalidURIError => e + raise RoutingError, e.message 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][1] - end - def routes_for_controller_and_action_and_keys(controller, action, keys) - routes.select do |route| - route.matches_controller_and_action? controller, action - end + env['action_controller.recognize'] = true + env['action_controller.rescue_error'] = rescue_error + status, headers, body = call(env) + body end # Subclasses and plugins may override this method to extract further attributes @@ -480,6 +591,109 @@ module ActionController def extract_request_environment(request) { :method => request.method } end + + private + def _uri(named_route, params, recall) + params = URISegment.wrap_values(params) + recall = URISegment.wrap_values(recall) + + unless result = @set.generate(:path_info, named_route, params, recall) + return + end + + uri, params = result + params.each do |k, v| + if v._value + params[k] = v._value + else + params.delete(k) + end + end + + uri << "?#{Rack::Mount::Utils.build_nested_query(params)}" if uri && params.any? + uri + end + + class URISegment < Struct.new(:_value, :_escape) + EXCLUDED = [:controller] + + def self.wrap_values(hash) + hash.inject({}) { |h, (k, v)| + h[k] = new(v, !EXCLUDED.include?(k.to_sym)) + h + } + end + + extend Forwardable + def_delegators :_value, :==, :eql?, :hash + + def to_param + @to_param ||= begin + if _value.is_a?(Array) + _value.map { |v| _escaped(v) }.join('/') + else + _escaped(_value) + end + end + end + alias_method :to_s, :to_param + + private + def _escaped(value) + v = value.respond_to?(:to_param) ? value.to_param : value + _escape ? Rack::Mount::Utils.escape_uri(v) : v.to_s + end + end + + def optionalize_trailing_dynamic_segments(path, requirements, defaults) + path = (path =~ /^\//) ? path.dup : "/#{path}" + optional, segments = true, [] + + required_segments = requirements.keys + required_segments -= defaults.keys.compact + + old_segments = path.split('/') + old_segments.shift + length = old_segments.length + + old_segments.reverse.each_with_index do |segment, index| + required_segments.each do |required| + if segment =~ /#{required}/ + optional = false + break + end + end + + if optional + if segment == ":id" && segments.include?(":action") + optional = false + elsif segment == ":controller" || segment == ":action" || segment == ":id" + # Ignore + elsif !(segment =~ /^:\w+$/) && + !(segment =~ /^:\w+\(\.:format\)$/) + optional = false + elsif segment =~ /^:(\w+)$/ + if defaults.has_key?($1.to_sym) + defaults.delete($1.to_sym) + else + optional = false + end + end + end + + if optional && index < length - 1 + segments.unshift('(/', segment) + segments.push(')') + elsif optional + segments.unshift('/(', segment) + segments.push(')') + else + segments.unshift('/', segment) + end + end + + segments.join + end end end end diff --git a/actionpack/lib/action_controller/routing/routing_ext.rb b/actionpack/lib/action_controller/routing/routing_ext.rb deleted file mode 100644 index 5e5b22b6c2..0000000000 --- a/actionpack/lib/action_controller/routing/routing_ext.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'active_support/core_ext/object/conversions' -require 'active_support/core_ext/boolean/conversions' -require 'active_support/core_ext/nil/conversions' -require 'active_support/core_ext/regexp' diff --git a/actionpack/lib/action_controller/routing/segments.rb b/actionpack/lib/action_controller/routing/segments.rb deleted file mode 100644 index 2603855476..0000000000 --- a/actionpack/lib/action_controller/routing/segments.rb +++ /dev/null @@ -1,343 +0,0 @@ -module ActionController - module Routing - class Segment #:nodoc: - RESERVED_PCHAR = ':@&=+$,;%' - SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}" - if RUBY_VERSION >= '1.9' - UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false).freeze - else - UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze - end - - # TODO: Convert :is_optional accessor to read only - attr_accessor :is_optional - alias_method :optional?, :is_optional - - def initialize - @is_optional = false - end - - def number_of_captures - Regexp.new(regexp_chunk).number_of_captures - 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_reader :value, :raw - alias_method :raw?, :raw - - def initialize(value = nil, options = {}) - super() - @value = value - @raw = options[:raw] if options.key?(:raw) - @is_optional = options[:optional] if options.key?(:optional) - end - - def interpolation_chunk - raw? ? value : super - end - - def regexp_chunk - chunk = Regexp.escape(value) - optional? ? Regexp.optionalize(chunk) : chunk - end - - def number_of_captures - 0 - 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, options = {}) - super(value, {:raw => true, :optional => true}.merge(options)) - end - - def optionality_implied? - true - end - end - - class DynamicSegment < Segment #:nodoc: - attr_reader :key - - # TODO: Convert these accessors to read only - attr_accessor :default, :regexp - - def initialize(key = nil, options = {}) - super() - @key = key - @default = options[:default] if options.key?(:default) - @regexp = options[:regexp] if options.key?(:regexp) - @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.to_s}\\Z" if regexp - end - - def regexp_chunk - regexp ? regexp_string : default_regexp_chunk - end - - def regexp_string - regexp_has_modifiers? ? "(#{regexp.to_s})" : "(#{regexp.source})" - end - - def default_regexp_chunk - "([^#{Routing::SEPARATORS.join}]+)" - end - - def number_of_captures - regexp ? regexp.number_of_captures + 1 : 1 - end - - def build_pattern(pattern) - pattern = "#{regexp_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 - - def regexp_has_modifiers? - regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0 - 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: - def interpolation_chunk(value_code = local_name) - "\#{#{value_code}}" - end - - def extract_value - "#{local_name} = hash[:#{key}] && Array(hash[:#{key}]).collect { |path_component| URI.escape(path_component.to_param, ActionController::Routing::Segment::UNSAFE_PCHAR) }.to_param #{"|| #{default.inspect}" if default}" - 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 default_regexp_chunk - "(.*)" - end - - def number_of_captures - regexp ? regexp.number_of_captures : 1 - 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 - - # The OptionalFormatSegment allows for any resource route to have an optional - # :format, which decreases the amount of routes created by 50%. - class OptionalFormatSegment < DynamicSegment - - def initialize(key = nil, options = {}) - super(:format, {:optional => true}.merge(options)) - end - - def interpolation_chunk - "." + super - end - - def regexp_chunk - '/|(\.[^/?\.]+)?' - end - - def to_s - '(.:format)?' - end - - def extract_value - "#{local_name} = options[:#{key}] && options[:#{key}].to_s.downcase" - end - - #the value should not include the period (.) - def match_extraction(next_capture) - %[ - if (m = match[#{next_capture}]) - params[:#{key}] = URI.unescape(m.from(1)) - end - ] - end - end - - end -end diff --git a/actionpack/lib/action_controller/routing/generation/url_rewriter.rb b/actionpack/lib/action_controller/routing/url_rewriter.rb index 52b66c9303..52b66c9303 100644 --- a/actionpack/lib/action_controller/routing/generation/url_rewriter.rb +++ b/actionpack/lib/action_controller/routing/url_rewriter.rb diff --git a/actionpack/lib/action_controller/testing/test_case.rb b/actionpack/lib/action_controller/testing/test_case.rb index 178e3477a6..01a55fe930 100644 --- a/actionpack/lib/action_controller/testing/test_case.rb +++ b/actionpack/lib/action_controller/testing/test_case.rb @@ -10,6 +10,13 @@ module ActionController self.session_options = TestSession::DEFAULT_OPTIONS.merge(:id => ActiveSupport::SecureRandom.hex(16)) end + class Result < ::Array #:nodoc: + def to_s() join '/' end + def self.new_escaped(strings) + new strings.collect {|str| URI.unescape str} + end + end + def assign_parameters(controller_path, action, parameters = {}) parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action) extra_keys = ActionController::Routing::Routes.extra_keys(parameters) @@ -18,7 +25,7 @@ module ActionController if value.is_a? Fixnum value = value.to_s elsif value.is_a? Array - value = ActionController::Routing::PathSegment::Result.new(value) + value = Result.new(value) end if extra_keys.include?(key.to_sym) diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 67448e66b9..cbfc8267f2 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -11,14 +11,6 @@ end ROUTING = ActionController::Routing -class ROUTING::RouteBuilder - attr_reader :warn_output - - def warn(msg) - (@warn_output ||= []) << msg - end -end - # See RFC 3986, section 3.3 for allowed path characters. class UriReservedCharactersRoutingTest < Test::Unit::TestCase def setup @@ -1626,25 +1618,6 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal '/pages/show/hello+world', default_route_set.generate(expected, expected) end - def test_parameter_shell - page_url = ROUTING::Route.new - page_url.requirements = {:controller => 'pages', :action => 'show', :id => /\d+/} - assert_equal({:controller => 'pages', :action => 'show'}, page_url.parameter_shell) - end - - def test_defaults - route = ROUTING::RouteBuilder.new.build '/users/:id.:format', :controller => "users", :action => "show", :format => "html" - assert_equal( - { :controller => "users", :action => "show", :format => "html" }, - route.defaults) - end - - def test_builder_complains_without_controller - assert_raise(ArgumentError) do - ROUTING::RouteBuilder.new.build '/contact', :contoller => "contact", :action => "index" - end - end - def test_build_empty_query_string assert_uri_equal '/foo', default_route_set.generate({:controller => 'foo'}) end |