Froze rails gems
[depot.git] / vendor / rails / actionpack / lib / action_controller / routing / route_set.rb
diff --git a/vendor/rails/actionpack/lib/action_controller/routing/route_set.rb b/vendor/rails/actionpack/lib/action_controller/routing/route_set.rb
new file mode 100644 (file)
index 0000000..1b8b52a
--- /dev/null
@@ -0,0 +1,437 @@
+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 = {})
+          if options.is_a?(Symbol)
+            if source_route = @set.named_routes.routes[options]
+              options = source_route.defaults.merge({ :conditions => source_route.conditions })
+            end
+          end
+          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, :configuration_file
+
+      def initialize
+        self.routes = []
+        self.named_routes = NamedRouteCollection.new
+
+        clear_recognize_optimized!
+      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
+        # This will force routing/recognition_optimization.rb
+        # to refresh optimisations.
+        clear_recognize_optimized!
+      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!
+      end
+
+      # reload! will always force a reload whereas load checks the timestamp first
+      alias reload! load!
+
+      def reload
+        if @routes_last_modified && configuration_file
+          mtime = File.stat(configuration_file).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 configuration_file
+          load configuration_file
+          @routes_last_modified = File.stat(configuration_file).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={})
+        raise "Not optimized! Check that routing/recognition_optimisation overrides RouteSet#recognize_path."
+      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