From b1999be5a7efd67e2602c37ed898aa8433661863 Mon Sep 17 00:00:00 2001
From: David Heinemeier Hansson <%=h @exception.message %>
+ Application error (Rails)
"
+ case exception
+ when RoutingError, UnknownAction then
+ render_text(IO.read(File.join(RAILS_ROOT, 'public', '404.html')), "404 Not Found")
+ else render_text "Application error (Rails)
"
+ end
end
# Overwrite to expand the meaning of a local request in order to show local rescues on other occurences than
@@ -66,7 +70,7 @@ module ActionController #:nodoc:
@contents = @template.render_file(template_path_for_local_rescue(exception), false)
@headers["Content-Type"] = "text/html"
- render_file(rescues_path("layout"), "500 Internal Error")
+ render_file(rescues_path("layout"), response_code_for_rescue(exception))
end
private
@@ -110,13 +114,21 @@ module ActionController #:nodoc:
rescues_path(
case exception
when MissingTemplate then "missing_template"
+ when RoutingError then "routing_error"
when UnknownAction then "unknown_action"
when ActionView::TemplateError then "template_error"
- else "diagnostics"
+ else raise ;"diagnostics"
end
)
end
+ def response_code_for_rescue(exception)
+ case exception
+ when UnknownAction, RoutingError then "404 Page Not Found"
+ else "500 Internal Error"
+ end
+ end
+
def clean_backtrace(exception)
exception.backtrace.collect { |line| Object.const_defined?(:RAILS_ROOT) ? line.gsub(RAILS_ROOT, "") : line }
end
diff --git a/actionpack/lib/action_controller/routing.rb b/actionpack/lib/action_controller/routing.rb
new file mode 100644
index 0000000000..39ce487e62
--- /dev/null
+++ b/actionpack/lib/action_controller/routing.rb
@@ -0,0 +1,260 @@
+module ActionController
+ module Routing
+ ROUTE_FILE = defined?(RAILS_ROOT) ? File.expand_path(File.join(RAILS_ROOT, 'config', 'routes')) : nil
+
+ class Route
+ attr_reader :defaults # The defaults hash
+
+ def initialize(path, hash={})
+ raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash)
+ @defaults = {}
+ @requirements = {}
+ self.items = path
+ hash.each do |k, v|
+ raise TypeError, "Hash may only contain symbols!" unless k.kind_of? Symbol
+ (@items.include?(k) ? @defaults : @requirements)[k] = v
+ end
+
+ # Add in defaults for :action and :id.
+ [[:action, 'index'], [:id, nil]].each do |name, default|
+ @defaults[name] = default if @items.include?(name) && ! (@requirements.key?(name) || @defaults.key?(name))
+ end
+ end
+
+ # Generate a URL given the provided options.
+ # All values in options should be symbols.
+ # Returns the path and the unused names in a 2 element array.
+ # If generation fails, [nil, nil] is returned
+ # Generation can fail because of a missing value, or because an equality check fails.
+ #
+ # Generate urls will be as short as possible. If the last component of a url is equal to the default value,
+ # then that component is removed. This is applied as many times as possible. So, your index controller's
+ # index action will generate []
+ def generate(options, defaults={})
+ non_matching = @requirements.inject([]) {|a, (k, v)| ((options[k] || defaults[k]) == v) ? a : a << k}
+ return nil, "Options mismatch requirements: #{non_matching.join ', '}" unless non_matching.empty?
+
+ used_names = @requirements.inject({}) {|hash, (k, v)| hash[k] = true; hash}
+ components = @items.collect do |item|
+ if item.kind_of? Symbol
+ used_names[item] = true
+ value = options[item] || defaults[item] || @defaults[item]
+ return nil, "#{item.inspect} was not given and has no default." if value.nil? && ! (@defaults.key?(item) && @defaults[item].nil?) # Don't leave if nil value.
+ defaults = {} unless defaults == {} || value == defaults[item] # Stop using defaults if this component isn't the same as the default.
+ value
+ else item
+ end
+ end
+
+ @items.reverse_each do |item| # Remove default components from the end of the generated url.
+ break unless item.kind_of?(Symbol) && @defaults[item] == components.last
+ components.pop
+ end
+
+ # If we have any nil components then we can't proceed.
+ # This might need to be changed. In some cases we may be able to return all componets after nil as extras.
+ missing = []; components.each_with_index {|c, i| missing << @items[i] if c.nil?}
+ return nil, "No values provided for component#{'s' if missing.length > 1} #{missing.join ', '} but values are required due to use of later components" unless missing.empty? # how wide is your screen?
+
+ unused = (options.keys - used_names.keys).inject({}) do |unused, key|
+ unused[key] = options[key] if options[key] != @defaults[key]
+ unused
+ end
+
+ components.collect! {|c| c.to_s}
+ components.unshift(components.shift + '/') if components.length == 1 && @items.first == :controller # Add '/' to controllers
+
+ return components, unused
+ end
+
+ # Recognize the provided path, returning a hash of recognized values, or [nil, reason] if the path isn't recognized.
+ # The path should be a list of component strings.
+ # Options is a hash of the ?k=v pairs
+ def recognize(components, options={})
+ options = options.clone
+ components = components.clone
+ controller_class = nil
+
+ @items.each do |item|
+ if item == :controller # Special case for controller
+ if components.empty? && @defaults[:controller]
+ controller_class, leftover = eat_path_to_controller(@defaults[:controller].split('/'))
+ raise RoutingError, "Default controller does not exist: #{@defaults[:controller]}" if controller_class.nil? || leftover.empty? == false
+ else
+ controller_class, remaining_components = eat_path_to_controller(components)
+ return nil, "No controller found at subpath #{components.join('/')}" if controller_class.nil?
+ components = remaining_components
+ end
+ options[:controller] = controller_class.controller_path
+ elsif item.kind_of? Symbol
+ value = components.shift || @defaults[item]
+ return nil, "No value or default for parameter #{item.inspect}" if value.nil? && ! (@defaults.key?(item) && @defaults[item].nil?)
+ options[item] = value
+ else
+ return nil, "No value available for component #{item.inspect}" if components.empty?
+ component = components.shift
+ return nil, "Value for component #{item.inspect} doesn't match #{component}" if component != item
+ end
+ end
+
+ if controller_class.nil? && @requirements[:controller] # Load a default controller
+ controller_class, extras = eat_path_to_controller(@requirements[:controller].split('/'))
+ raise RoutingError, "Illegal controller path for route default: #{@requirements[:controller]}" unless controller_class && extras.empty?
+ options[:controller] = controller_class.controller_path
+ end
+ options = @requirements.merge(options)
+
+ return nil, "Route recognition didn't find a controller class!" unless controller_class
+ return nil, "Unused components were left: #{components.join '/'}" unless components.empty?
+ options.delete_if {|k, v| v.nil?} # Remove nil values.
+ return controller_class, options
+ end
+
+ def inspect
+ when_str = @requirements.empty? ? "" : " when #{@requirements.inspect}"
+ default_str = @defaults.empty? ? "" : " || #{@defaults.inspect}"
+ "<#{self.class.to_s} #{@items.collect{|c| c.kind_of?(String) ? c : c.inspect}.join('/').inspect}#{default_str}#{when_str}>"
+ end
+
+ protected
+ # Find the controller given a list of path components.
+ # Return the controller class and the unused path components.
+ def eat_path_to_controller(path)
+ path.inject([Controllers, 1]) do |(mod, length), name|
+ name = name.camelize
+ controller_name = name + "Controller"
+ return mod.const_get(controller_name), path[length..-1] if mod.const_available? controller_name
+ return nil, nil unless mod.const_available? name
+ [mod.const_get(name), length + 1]
+ end
+ return nil, nil # Path ended, but no controller found.
+ end
+
+ def items=(path)
+ items = path.split('/').collect {|c| (/^:(\w+)$/ =~ c) ? $1.intern : c} if path.kind_of?(String) # split and convert ':xyz' to symbols
+ items.shift if items.first == ""
+ items.pop if items.last == ""
+ @items = items
+
+ # Verify uniqueness of each component.
+ @items.inject({}) do |seen, item|
+ if item.kind_of? Symbol
+ raise ArgumentError, "Illegal route path -- duplicate item #{item}\n #{path.inspect}" if seen.key? item
+ seen[item] = true
+ end
+ seen
+ end
+ end
+ end
+
+ class RouteSet
+ def initialize
+ @routes = []
+ end
+
+ def add_route(route)
+ raise TypeError, "#{route.inspect} is not a Route instance!" unless route.kind_of?(Route)
+ @routes << route
+ end
+ def empty?
+ @routes.empty?
+ end
+ def each
+ @routes.each {|route| yield route}
+ end
+
+ # Generate a path for the provided options
+ # Returns the path as an array of components and a hash of unused names
+ # Raises RoutingError if not route can handle the provided components.
+ #
+ # Note that we don't return the first generated path. We do this so that when a route
+ # generates a path from a subset of the available options we can keep looking for a
+ # route which can generate a path that uses more options.
+ # Note that we *do* return immediately if
+ def generate(options, request)
+ raise RoutingError, "There are no routes defined!" if @routes.empty?
+ options = options.symbolize_keys
+ defaults = request.path_parameters.symbolize_keys
+ expand_controller_path!(options, defaults)
+
+ failures = []
+ selected = nil
+ self.each do |route|
+ path, unused = route.generate(options, defaults)
+ if path.nil?
+ failures << [route, unused] if ActionController::Base.debug_routes
+ else
+ return path, unused if unused.empty? # Found a perfect route -- we're finished.
+ if selected.nil? || unused.length < selected.last.length
+ failures << [selected.first, "A better url than #{selected[1]} was found."] if selected
+ selected = [route, path, unused]
+ end
+ end
+ end
+
+ return selected[1..-1] unless selected.nil?
+ raise RoutingError.new("Generation failure: No route for url_options #{options.inspect}, defaults: #{defaults.inspect}", failures)
+ end
+
+ # Recognize the provided path.
+ # Raise RoutingError if the path can't be recognized.
+ def recognize!(request)
+ path = ((%r{^/?(.*)/?$} =~ request.path) ? $1 : request.path).split('/')
+ raise RoutingError, "There are no routes defined!" if @routes.empty?
+
+ failures = []
+ self.each do |route|
+ controller, options = route.recognize(path)
+ if controller.nil?
+ failures << [route, options] if ActionController::Base.debug_routes
+ else
+ options.each {|k, v| request.path_parameters[k] = CGI.unescape(v)}
+ return controller
+ end
+ end
+
+ raise RoutingError.new("No route for path: #{path.join('/').inspect}", failures)
+ end
+
+ def expand_controller_path!(options, defaults)
+ if options[:controller]
+ if /^\// =~ options[:controller]
+ options[:controller] = options[:controller][1..-1]
+ defaults.clear # Sending to absolute controller implies fresh defaults
+ else
+ relative_to = defaults[:controller] ? defaults[:controller].split('/')[0..-2].join('/') : ''
+ options[:controller] = relative_to.empty? ? options[:controller] : "#{relative_to}/#{options[:controller]}"
+ end
+ else
+ options[:controller] = defaults[:controller]
+ end
+ end
+
+ def route(*args)
+ add_route(Route.new(*args))
+ end
+ alias :connect :route
+
+ def reload
+ begin require_dependency(ROUTE_FILE)
+ rescue LoadError, ScriptError => e
+ raise RoutingError, "Cannot load config/routes.rb:\n #{e.message}"
+ ensure # Ensure that there is at least one route:
+ connect(':controller/:action/:id', :action => 'index', :id => nil) if @routes.empty?
+ end
+ end
+
+ def draw
+ @routes.clear
+ yield self
+ end
+ end
+
+ def self.draw(*args, &block)
+ Routes.draw(*args) {|*args| block.call(*args)}
+ end
+
+ Routes = RouteSet.new
+ #Routes.reload # Do this here, so that server will die on load if SyntaxError or whatnot.
+ end
+end
\ No newline at end of file
diff --git a/actionpack/lib/action_controller/scaffolding.rb b/actionpack/lib/action_controller/scaffolding.rb
index 9c1311efa3..140df73972 100644
--- a/actionpack/lib/action_controller/scaffolding.rb
+++ b/actionpack/lib/action_controller/scaffolding.rb
@@ -149,7 +149,7 @@ module ActionController
private
def render#{suffix}_scaffold(action = caller_method_name(caller))
- if template_exists?("\#{controller_name}/\#{action}")
+ if template_exists?("\#{self.class.controller_path}/\#{action}")
render_action(action)
else
@scaffold_class = #{class_name}
diff --git a/actionpack/lib/action_controller/templates/rescues/routing_error.rhtml b/actionpack/lib/action_controller/templates/rescues/routing_error.rhtml
new file mode 100644
index 0000000000..82c01e10c9
--- /dev/null
+++ b/actionpack/lib/action_controller/templates/rescues/routing_error.rhtml
@@ -0,0 +1,8 @@
+Routing Error
+Failure reasons:
+ <% @exception.failures.each do |route, reason| %>
+ <%=h route.inspect.gsub('\\', '') %> failed because <%=h reason.downcase %>
+ <% end %>
+
Beautiful modules!
", process_request.body + def test_nested_rendering + @request.action = "hello_world" + assert_equal "Living in a nested world", Fun::GamesController.process(@request, @response).body end private diff --git a/actionpack/test/controller/request_test.rb b/actionpack/test/controller/request_test.rb index ebab660eab..c31cdd460b 100644 --- a/actionpack/test/controller/request_test.rb +++ b/actionpack/test/controller/request_test.rb @@ -31,6 +31,28 @@ class RequestTest < Test::Unit::TestCase @request.port = 8080 assert_equal ":8080", @request.port_string end + + def test_request_uri + @request.set_REQUEST_URI "http://www.rubyonrails.org/path/of/some/uri?mapped=1" + assert_equal "/path/of/some/uri?mapped=1", @request.request_uri + assert_equal "/path/of/some/uri", @request.path + + @request.set_REQUEST_URI "http://www.rubyonrails.org/path/of/some/uri" + assert_equal "/path/of/some/uri", @request.request_uri + assert_equal "/path/of/some/uri", @request.path + + @request.set_REQUEST_URI "/path/of/some/uri" + assert_equal "/path/of/some/uri", @request.request_uri + assert_equal "/path/of/some/uri", @request.path + + @request.set_REQUEST_URI "/" + assert_equal "/", @request.request_uri + assert_equal "/", @request.path + + @request.set_REQUEST_URI "/?m=b" + assert_equal "/?m=b", @request.request_uri + assert_equal "/", @request.path + end def test_host_with_port @request.env['HTTP_HOST'] = "rubyonrails.org:8080" diff --git a/actionpack/test/controller/routing_tests.rb b/actionpack/test/controller/routing_tests.rb new file mode 100644 index 0000000000..cdc9c4e53c --- /dev/null +++ b/actionpack/test/controller/routing_tests.rb @@ -0,0 +1,409 @@ +# Code Generated by ZenTest v. 2.3.0 +# Couldn't find class for name Routing +# classname: asrt / meth = ratio% +# ActionController::Routing::RouteSet: 0 / 16 = 0.00% +# ActionController::Routing::RailsRoute: 0 / 4 = 0.00% +# ActionController::Routing::Route: 0 / 8 = 0.00% + +RAILS_ROOT = "" +require File.dirname(__FILE__) + '/../abstract_unit' +require 'test/unit' +require 'cgi' + +class FakeController + attr_reader :controller_path + attr_reader :name + def initialize(name, controller_path) + @name = name + @controller_path = controller_path + end + def kind_of?(x) + x === Class || x == FakeController + end +end + +module Controllers + module Admin + UserController = FakeController.new 'Admin::UserController', 'admin/user' + AccessController = FakeController.new 'Admin::AccessController', 'admin/access' + end + module Editing + PageController = FakeController.new 'Editing::PageController', 'editing/page' + ImageController = FakeController.new 'Editing::ImageController', 'editing/image' + end + module User + NewsController = FakeController.new 'User::NewsController', 'user/news' + PaymentController = FakeController.new 'User::PaymentController', 'user/payment' + end + ContentController = FakeController.new 'ContentController', 'content' + ResourceController = FakeController.new 'ResourceController', 'resource' +end + +# Extend the modules with the required methods... +[Controllers, Controllers::Admin, Controllers::Editing, Controllers::User].each do |mod| + mod.instance_eval('alias :const_available? :const_defined?') + mod.constants.each {|k| Object.const_set(k, mod.const_get(k))} # export the modules & controller classes. +end + + +class RouteTests < Test::Unit::TestCase + def route(*args) + return @route if @route && (args.empty? || @args == args) + @args = args + @route = ActionController::Routing::Route.new(*args) + return @route + end + + def setup + self.route '/:controller/:action/:id' + @defaults = {:controller => 'content', :action => 'show', :id => '314'} + end + + # Don't put a leading / on the url. + # Make sure the controller is one from the above fake Controllers module. + def verify_recognize(url, expected_options, reason='') + url = url.split('/') if url.kind_of? String + reason = ": #{reason}" unless reason.empty? + controller_class, options = @route.recognize(url) + assert_not_equal nil, controller_class, "#{@route.inspect} didn't recognize #{url}#{reason}\n #{options}" + assert_equal expected_options, options, "#{@route.inspect} produced wrong options for #{url}#{reason}" + end + + # The expected url should not have a leading / + # You can use @defaults if you want a set of plausible defaults + def verify_generate(expected_url, expected_extras, options, defaults, reason='') + reason = "#{reason}: " unless reason.empty? + components, extras = @route.generate(options, defaults) + assert_not_equal nil, components, "#{reason}#{@route.inspect} didn't generate for \n options = #{options.inspect}\n defaults = #{defaults.inspect}\n #{extras}" + assert_equal expected_extras, extras, "#{reason} #{@route.inspect}.generate: incorrect extra's" + assert_equal expected_url, components.join('/'), "#{reason} #{@route.inspect}.generate: incorrect url" + end + + def test_recognize_default_unnested_with_action_and_id + verify_recognize('content/action/id', {:controller => 'content', :action => 'action', :id => 'id'}) + verify_recognize('content/show/10', {:controller => 'content', :action => 'show', :id => '10'}) + end + def test_generate_default_unnested_with_action_and_id_no_extras + verify_generate('content/action/id', {}, {:controller => 'content', :action => 'action', :id => 'id'}, @defaults) + verify_generate('content/show/10', {}, {:controller => 'content', :action => 'show', :id => '10'}, @defaults) + end + def test_generate_default_unnested_with_action_and_id + verify_generate('content/action/id', {:a => 'a'}, {:controller => 'content', :action => 'action', :id => 'id', :a => 'a'}, @defaults) + verify_generate('content/show/10', {:a => 'a'}, {:controller => 'content', :action => 'show', :id => '10', :a => 'a'}, @defaults) + end + + # Note that we can't put tests here for proper relative controller handline + # because that is handled by RouteSet. + def test_recognize_default_nested_with_action_and_id + verify_recognize('admin/user/action/id', {:controller => 'admin/user', :action => 'action', :id => 'id'}) + verify_recognize('admin/user/show/10', {:controller => 'admin/user', :action => 'show', :id => '10'}) + end + def test_generate_default_nested_with_action_and_id_no_extras + verify_generate('admin/user/action/id', {}, {:controller => 'admin/user', :action => 'action', :id => 'id'}, @defaults) + verify_generate('admin/user/show/10', {}, {:controller => 'admin/user', :action => 'show', :id => '10'}, @defaults) + end + def test_generate_default_nested_with_action_and_id_relative_to_root + verify_generate('admin/user/action/id', {:a => 'a'}, {:controller => 'admin/user', :action => 'action', :id => 'id', :a => 'a'}, @defaults) + verify_generate('admin/user/show/10', {:a => 'a'}, {:controller => 'admin/user', :action => 'show', :id => '10', :a => 'a'}, @defaults) + end + + def test_recognize_default_nested_with_action + verify_recognize('admin/user/action', {:controller => 'admin/user', :action => 'action'}) + verify_recognize('admin/user/show', {:controller => 'admin/user', :action => 'show'}) + end + def test_generate_default_nested_with_action_no_extras + verify_generate('admin/user/action', {}, {:controller => 'admin/user', :action => 'action'}, @defaults) + verify_generate('admin/user/show', {}, {:controller => 'admin/user', :action => 'show'}, @defaults) + end + def test_generate_default_nested_with_action + verify_generate('admin/user/action', {:a => 'a'}, {:controller => 'admin/user', :action => 'action', :a => 'a'}, @defaults) + verify_generate('admin/user/show', {:a => 'a'}, {:controller => 'admin/user', :action => 'show', :a => 'a'}, @defaults) + end + + def test_recognize_default_nested_with_id_and_index + verify_recognize('admin/user/index/hello', {:controller => 'admin/user', :id => 'hello', :action => 'index'}) + verify_recognize('admin/user/index/10', {:controller => 'admin/user', :id => "10", :action => 'index'}) + end + def test_generate_default_nested_with_id_no_extras + verify_generate('admin/user/index/hello', {}, {:controller => 'admin/user', :id => 'hello'}, @defaults) + verify_generate('admin/user/index/10', {}, {:controller => 'admin/user', :id => 10}, @defaults) + end + def test_generate_default_nested_with_id + verify_generate('admin/user/index/hello', {:a => 'a'}, {:controller => 'admin/user', :id => 'hello', :a => 'a'}, @defaults) + verify_generate('admin/user/index/10', {:a => 'a'}, {:controller => 'admin/user', :id => 10, :a => 'a'}, @defaults) + end + + def test_recognize_default_nested + verify_recognize('admin/user', {:controller => 'admin/user', :action => 'index'}) + verify_recognize('admin/user', {:controller => 'admin/user', :action => 'index'}) + end + def test_generate_default_nested_no_extras + verify_generate('admin/user/', {}, {:controller => 'admin/user'}, @defaults) + verify_generate('admin/user/', {}, {:controller => 'admin/user'}, @defaults) + end + def test_generate_default_nested + verify_generate('admin/user/', {:a => 'a'}, {:controller => 'admin/user', :a => 'a'}, @defaults) + verify_generate('admin/user/', {:a => 'a'}, {:controller => 'admin/user', :a => 'a'}, @defaults) + end + + # Test generate with a default controller set. + def test_generate_default_controller + route '/:controller/:action/:id', :action => 'index', :id => nil, :controller => 'content' + @defaults[:controller] = 'resource' + + verify_generate('', {}, {:controller => 'content'}, @defaults) + verify_generate('', {}, {:controller => 'content', :action => 'index'}, @defaults) + verify_generate('content/not-index', {}, {:controller => 'content', :action => 'not-index'}, @defaults) + verify_generate('content/index/10', {}, {:controller => 'content', :id => 10}, @defaults) + verify_generate('content/index/hi', {}, {:controller => 'content', :action => 'index', :id => 'hi'}, @defaults) + verify_generate('', {:a => 'a'}, {:controller => 'content', :a => 'a'}, @defaults) + verify_generate('', {:a => 'a'}, {:controller => 'content', :a => 'a'}, @defaults) + + # Call some other generator tests + test_generate_default_unnested_with_action_and_id + test_generate_default_nested_with_action_and_id_no_extras + test_generate_default_nested_with_id + test_generate_default_nested_with_id_no_extras + end + + # Test generate with a default controller set. + def test_generate_default_controller + route '/:controller/:action/:id', :action => 'index', :id => nil, :controller => 'content' + @defaults[:controller] = 'resource' + verify_recognize('', {:controller => 'content', :action => 'index'}) + verify_recognize('content', {:controller => 'content', :action => 'index'}) + verify_recognize('content/index', {:controller => 'content', :action => 'index'}) + verify_recognize('content/index/10', {:controller => 'content', :action => 'index', :id => '10'}) + end + # Make sure generation & recognition don't happen in some cases: + def test_no_generate_on_no_options + assert_equal nil, @route.generate({}, {})[0] + end + def test_requirements + route 'some_static/route', :controller => 'content' + assert_equal nil, @route.generate({}, {})[0] + assert_equal nil, @route.generate({:controller => "dog"}, {})[0] + assert_equal nil, @route.recognize([])[0] + assert_equal nil, @route.recognize(%w{some_static route with more than expected})[0] + end + + def test_basecamp + route 'clients/', :controller => 'content' + verify_generate('clients', {}, {:controller => 'content'}, {}) # Would like to have clients/ + verify_generate('clients', {}, {:controller => 'content'}, @defaults) + end + + def test_basecamp2 + route 'clients/:client_name/:project_name/', :controller => 'content', :action => 'start_page_redirect' + verify_recognize('clients/projects/2', {:controller => 'content', :client_name => 'projects', :project_name => '2', :action => 'start_page_redirect'}) + end + + def test_xal_style_dates + route 'articles/:category/:year/:month/:day', :controller => 'content', :action => 'list_articles', :category => 'all', :year => nil, :month => nil, :day =>nil + verify_recognize('articles', {:controller => 'content', :action => 'list_articles', :category => 'all'}) + verify_recognize('articles/porn', {:controller => 'content', :action => 'list_articles', :category => 'porn'}) + verify_recognize('articles/news/2005/08', {:controller => 'content', :action => 'list_articles', :category => 'news', :year => '2005', :month => '08'}) + verify_recognize('articles/news/2005/08/04', {:controller => 'content', :action => 'list_articles', :category => 'news', :year => '2005', :month => '08', :day => '04'}) + assert_equal nil, @route.recognize(%w{articles too many components are here})[0] + assert_equal nil, @route.recognize('')[0] + + verify_generate('articles', {}, {:controller => 'content', :action => 'list_articles'}, @defaults) + verify_generate('articles', {}, {:controller => 'content', :action => 'list_articles', :category => 'all'}, @defaults) + verify_generate('articles/news', {}, {:controller => 'content', :action => 'list_articles', :category => 'news'}, @defaults) + verify_generate('articles/news/2005', {}, {:controller => 'content', :action => 'list_articles', :category => 'news', :year => '2005'}, @defaults) + verify_generate('articles/news/2005/05', {}, {:controller => 'content', :action => 'list_articles', :category => 'news', :year => '2005', :month => '05'}, @defaults) + verify_generate('articles/news/2005/05/16', {}, {:controller => 'content', :action => 'list_articles', :category => 'news', :year => '2005', :month => '05', :day => '16'}, @defaults) + + assert_equal nil, @route.generate({:controller => 'content', :action => 'list_articles', :day => '2'}, @defaults)[0] + # The above case should fail because a nil value cannot be present in a path. + # In other words, since :day is given, :month and :year must be given too. + end + + + def test_no_controller + route 'some/:special/:route', :controller => 'a/missing/controller', :action => 'anything' + assert_raises(ActionController::RoutingError, "Should raise due to nonexistant controller") {@route.recognize(%w{some matching path})} + end + def test_bad_controller_path + assert_equal nil, @route.recognize(%w{no such controller fake_action id})[0] + end + def test_too_short_path + assert_equal nil, @route.recognize([])[0] + route 'some/static/route', :controller => 'content', :action => 'show' + assert_equal nil, route.recognize([])[0] + end + def test_too_long_path + assert_equal nil, @route.recognize(%w{content action id some extra components})[0] + end + def test_incorrect_static_component + route 'some/static/route', :controller => 'content', :action => 'show' + assert_equal nil, route.recognize(%w{an non_matching path})[0] + end + def test_no_controller_defined + route 'some/:path/:without/a/controller' + assert_equal nil, route.recognize(%w{some matching path a controller})[0] + end + + def test_mismatching_requirements + route 'some/path', :controller => 'content', :action => 'fish' + assert_equal nil, route.generate({:controller => 'admin/user', :action => 'list'})[0] + assert_equal nil, route.generate({:controller => 'content', :action => 'list'})[0] + assert_equal nil, route.generate({:controller => 'admin/user', :action => 'fish'})[0] + end + + def test_missing_value_for_generate + assert_equal nil, route.generate({})[0] # :controller is missing + end + def test_nils_inside_generated_path + route 'show/:year/:month/:day', :month => nil, :day => nil, :controller => 'content', :action => 'by_date' + assert_equal nil, route.generate({:year => 2005, :day => 10})[0] + end + + def test_expand_controller_path_non_nested_no_leftover + controller, leftovers = @route.send :eat_path_to_controller, %w{content} + assert_equal Controllers::ContentController, controller + assert_equal [], leftovers + end + def test_expand_controller_path_non_nested_with_leftover + controller, leftovers = @route.send :eat_path_to_controller, %w{content action id} + assert_equal Controllers::ContentController, controller + assert_equal %w{action id}, leftovers + end + def test_expand_controller_path_nested_no_leftover + controller, leftovers = @route.send :eat_path_to_controller, %w{admin user} + assert_equal Controllers::Admin::UserController, controller + assert_equal [], leftovers + end + def test_expand_controller_path_nested_no_leftover + controller, leftovers = @route.send :eat_path_to_controller, %w{admin user action id} + assert_equal Controllers::Admin::UserController, controller + assert_equal %w{action id}, leftovers + end +end + +class RouteSetTests < Test::Unit::TestCase + def setup + @set = ActionController::Routing::RouteSet.new + @rails_route = ActionController::Routing::Route.new '/:controller/:action/:id', :action => 'index', :id => nil + @request = ActionController::TestRequest.new({}, {}, nil) + end + def test_emptyness + assert_equal true, @set.empty?, "New RouteSets should respond to empty? with true." + @set.each { flunk "New RouteSets should be empty." } + end + def test_add_illegal_route + assert_raises(TypeError) {@set.add_route "I'm not actually a route."} + end + def test_add_normal_route + @set.add_route @rails_route + seen = false + @set.each do |route| + assert_equal @rails_route, route + flunk("Each should have yielded only a single route!") if seen + seen = true + end + end + + def test_expand_controller_path_non_relative + defaults = {:controller => 'admin/user', :action => 'list'} + options = {:controller => '/content'} + @set.expand_controller_path!(options, defaults) + assert_equal({:controller => 'content'}, options) + end + def test_expand_controller_path_relative_to_nested + defaults = {:controller => 'admin/user', :action => 'list'} + options = {:controller => 'access'} + @set.expand_controller_path!(options, defaults) + assert_equal({:controller => 'admin/access'}, options) + end + def test_expand_controller_path_relative_to_root + defaults = {:controller => 'content', :action => 'list'} + options = {:controller => 'resource'} + @set.expand_controller_path!(options, defaults) + assert_equal({:controller => 'resource'}, options) + end + def test_expand_controller_path_into_module + defaults = {:controller => 'content', :action => 'list'} + options = {:controller => 'admin/user'} + @set.expand_controller_path!(options, defaults) + assert_equal({:controller => 'admin/user'}, options) + end + def test_expand_controller_path_switch_module_with_absolute + defaults = {:controller => 'user/news', :action => 'list'} + options = {:controller => '/admin/user'} + @set.expand_controller_path!(options, defaults) + assert_equal({:controller => 'admin/user'}, options) + end + def test_expand_controller_no_default + options = {:controller => 'content'} + @set.expand_controller_path!(options, {}) + assert_equal({:controller => 'content'}, options) + end + + # Don't put a leading / on the url. + # Make sure the controller is one from the above fake Controllers module. + def verify_recognize(expected_controller, expected_path_parameters=nil, path=nil) + @set.add_route(@rails_route) if @set.empty? + @request.path = path if path + controller = @set.recognize!(@request) + assert_equal expected_controller, controller + assert_equal expected_path_parameters, @request.path_parameters if expected_path_parameters + end + + # The expected url should not have a leading / + # You can use @defaults if you want a set of plausible defaults + def verify_generate(expected_url, options, expected_extras={}) + @set.add_route(@rails_route) if @set.empty? + components, extras = @set.generate(options, @request) + assert_equal expected_extras, extras, "#incorrect extra's" + assert_equal expected_url, components.join('/'), "incorrect url" + end + def typical_request + @request.path_parameters = {:controller => 'content', :action => 'show', :id => '10'} + end + def typical_nested_request + @request.path_parameters = {:controller => 'admin/user', :action => 'grant', :id => '02seckar'} + end + + def test_generate_typical_controller_action_path + typical_request + verify_generate('content/list', {:controller => 'content', :action => 'list'}) + end + def test_generate_typical_controller_index_path_explicit_index + typical_request + verify_generate('content/', {:controller => 'content', :action => 'index'}) + end + def test_generate_typical_controller_index_path_explicit_index + typical_request + verify_generate('content/', {:controller => 'content', :action => 'index'}) + end + def test_generate_typical_controller_index_path_implicit_index + typical_request + @request.path_parameters[:controller] = 'resource' + verify_generate('content/', {:controller => 'content'}) + end + + def test_generate_no_perfect_route + typical_request + verify_generate('admin/user/show/43seckar', {:controller => 'admin/user', :action => 'show', :id => '43seckar', :likes_fishing => 'fuzzy(0.3)'}, {:likes_fishing => 'fuzzy(0.3)'}) + end + + def test_generate_no_match + @set.add_route(@rails_route) + @request.path_parameters = {} + assert_raises(ActionController::RoutingError) {@set.generate({}, @request)} + end + + + def test_encoded_strings + verify_recognize(Controllers::Admin::UserController, {:controller => 'admin/user', :action => 'info', :id => "Nicholas Seckar"}, path='/admin/user/info/Nicholas%20Seckar') + end +end + +#require '../assertions/action_pack_assertions.rb' +class AssertionRoutingTests < Test::Unit::TestCase + def test_assert_routing + ActionController::Routing::Routes.reload rescue nil + assert_routing('content/', {:controller => 'content', :action => 'index'}) + end +end diff --git a/actionpack/test/controller/url_obsolete.rb b/actionpack/test/controller/url_obsolete.rb new file mode 100644 index 0000000000..4b6544dbf7 --- /dev/null +++ b/actionpack/test/controller/url_obsolete.rb @@ -0,0 +1,487 @@ +require File.dirname(__FILE__) + '/../abstract_unit' +require 'action_controller/url_rewriter' + +MockRequest = Struct.new("MockRequest", :protocol, :host, :port, :path, :parameters, :path_parameters) +class MockRequest + def host_with_port + if (protocol == "http://" && port == 80) || (protocol == "https://" && port == 443) + host + else + host + ":#{port}" + end + end +end + +class UrlMockFactory + def self.create(path, parameters) + ActionController::UrlRewriter.new( + MockRequest.new("http://", "example.com", 80, path, parameters), + parameters + ) + end +end + +# old-style support for .new +module ActionController + class UrlRewriter + def self.old_new(request, controller, action) + request.parameters[:controller] = controller + request.parameters[:action] = action + return new(request, request.parameters) + end + end +end +class UrlTest < Test::Unit::TestCase + def setup + @library_url = ActionController::UrlRewriter.old_new(MockRequest.new( + "http://", + "www.singlefile.com", + 80, + "/library/books/ISBN/0743536703/show", + { "type" => "ISBN", "code" => "0743536703" } + ), "books", "show") + + @library_url_using_module = ActionController::UrlRewriter.old_new(MockRequest.new( + "http://", + "www.singlefile.com", + 80, + "/library/books/ISBN/0743536703/show", + { "type" => "ISBN", "code" => "0743536703", "module" => "library" } + ), "books", "show") + + @library_url_on_index = ActionController::UrlRewriter.old_new(MockRequest.new( + "http://", + "www.singlefile.com", + 80, + "/library/books/ISBN/0743536703/", + { "type" => "ISBN", "code" => "0743536703" } + ), "books", "index") + + @clean_urls = [ + ActionController::UrlRewriter.old_new(MockRequest.new( + "http://", "www.singlefile.com", 80, "/identity/", {} + ), "identity", "index"), + ActionController::UrlRewriter.old_new(MockRequest.new( + "http://", "www.singlefile.com", 80, "/identity", {} + ), "identity", "index") + ] + + @clean_url_with_id = ActionController::UrlRewriter.old_new(MockRequest.new( + "http://", "www.singlefile.com", 80, "/identity/show/5", { "id" => "5" } + ), "identity", "show") + + @clean_url_with_same_action_and_controller_name = ActionController::UrlRewriter.old_new(MockRequest.new( + "http://", "www.singlefile.com", 80, "/login/login", { } + ), "login", "login") + + @clean_url_with_same_action_and_controller_and_module_name = ActionController::UrlRewriter.old_new(MockRequest.new( + "http://", "www.singlefile.com", 80, "/login/login/login", { "module" => "login" } + ), "login", "login") + + @clean_url_with_id_as_char = ActionController::UrlRewriter.old_new(MockRequest.new( + "http://", "www.singlefile.com", 80, "/teachers/show/t", { "id" => "t" } + ), "teachers", "show") + end + + def test_clean_action + assert_equal "http://www.singlefile.com/library/books/ISBN/0743536703/edit", @library_url.rewrite(:action => "edit") + end + + def test_clean_action_to_another_host + assert_equal( + "http://www.booksphere.com/library/books/ISBN/0743536703/edit", + @library_url.rewrite(:action => "edit", :host => "www.booksphere.com") + ) + end + + def test_clean_action_to_another_host_and_protocol + assert_equal( + "https://www.booksphere.com/library/books/ISBN/0743536703/edit", + @library_url.rewrite(:action => "edit", :host => "www.booksphere.com", :protocol => "https://") + ) + end + + def test_clean_action_with_only_path + assert_equal "/library/books/ISBN/0743536703/edit", @library_url.rewrite(:action => "edit", :only_path => true) + end + + def test_action_from_index + assert_equal "http://www.singlefile.com/library/books/ISBN/0743536703/edit", @library_url_on_index.rewrite(:action => "edit") + end + + def test_action_from_index_on_clean + @clean_urls.each do |url| + assert_equal "http://www.singlefile.com/identity/edit", url.rewrite(:action => "edit") + end + end + + def test_action_without_prefix + assert_equal "http://www.singlefile.com/library/books/", @library_url.rewrite(:action => "index", :action_prefix => "") + end + + def test_action_with_prefix + assert_equal( + "http://www.singlefile.com/library/books/XTC/123/show", + @library_url.rewrite(:action => "show", :action_prefix => "XTC/123") + ) + end + + def test_action_prefix_alone + assert_equal( + "http://www.singlefile.com/library/books/XTC/123/", + @library_url.rewrite(:action_prefix => "XTC/123") + ) + end + + def test_action_with_suffix + assert_equal( + "http://www.singlefile.com/library/books/show/XTC/123", + @library_url.rewrite(:action => "show", :action_prefix => "", :action_suffix => "XTC/123") + ) + end + + def test_clean_controller + assert_equal "http://www.singlefile.com/library/settings/", @library_url.rewrite(:controller => "settings") + end + + def test_clean_controller_prefix + assert_equal "http://www.singlefile.com/shop/", @library_url.rewrite(:controller_prefix => "shop") + end + + def test_clean_controller_with_module + assert_equal "http://www.singlefile.com/shop/purchases/", @library_url.rewrite(:module => "shop", :controller => "purchases") + end + + def test_getting_out_of_a_module + assert_equal "http://www.singlefile.com/purchases/", @library_url_using_module.rewrite(:module => false, :controller => "purchases") + end + + def test_controller_and_action + assert_equal "http://www.singlefile.com/library/settings/show", @library_url.rewrite(:controller => "settings", :action => "show") + end + + def test_controller_and_action_and_anchor + assert_equal( + "http://www.singlefile.com/library/settings/show#5", + @library_url.rewrite(:controller => "settings", :action => "show", :anchor => "5") + ) + end + + def test_controller_and_action_and_empty_overwrite_params_and_anchor + assert_equal( + "http://www.singlefile.com/library/settings/show?code=0743536703&type=ISBN#5", + @library_url.rewrite(:controller => "settings", :action => "show", :overwrite_params => {}, :anchor => "5") + ) + end + + def test_controller_and_action_and_overwrite_params_and_anchor + assert_equal( + "http://www.singlefile.com/library/settings/show?code=0000001&type=ISBN#5", + @library_url.rewrite(:controller => "settings", :action => "show", :overwrite_params => {"code"=>"0000001"}, :anchor => "5") + ) + end + + def test_controller_and_action_and_overwrite_params_with_nil_value_and_anchor + assert_equal( + "http://www.singlefile.com/library/settings/show?type=ISBN#5", + @library_url.rewrite(:controller => "settings", :action => "show", :overwrite_params => {"code" => nil}, :anchor => "5") + ) + end + + def test_controller_and_action_params_and_overwrite_params_and_anchor + assert_equal( + "http://www.singlefile.com/library/settings/show?code=0000001&version=5.0#5", + @library_url.rewrite(:controller => "settings", :action => "show", :params=>{"version" => "5.0"}, :overwrite_params => {"code"=>"0000001"}, :anchor => "5") + ) + end + + def test_controller_and_action_and_params_anchor + assert_equal( + "http://www.singlefile.com/library/settings/show?update=1#5", + @library_url.rewrite(:controller => "settings", :action => "show", :params => { "update" => "1"}, :anchor => "5") + ) + end + + def test_controller_and_index_action + assert_equal "http://www.singlefile.com/library/settings/", @library_url.rewrite(:controller => "settings", :action => "index") + end + + def test_same_controller_and_action_names + assert_equal "http://www.singlefile.com/login/logout", @clean_url_with_same_action_and_controller_name.rewrite(:action => "logout") + end + + def xtest_same_module_and_controller_and_action_names + assert_equal "http://www.singlefile.com/login/login/logout", @clean_url_with_same_action_and_controller_and_module_name.rewrite(:action => "logout") + end + + def test_controller_and_action_with_same_name_as_controller + @clean_urls.each do |url| + assert_equal "http://www.singlefile.com/anything/identity", url.rewrite(:controller => "anything", :action => "identity") + end + end + + def test_controller_and_index_action_without_controller_prefix + assert_equal( + "http://www.singlefile.com/settings/", + @library_url.rewrite(:controller => "settings", :action => "index", :controller_prefix => "") + ) + end + + def test_controller_and_index_action_with_controller_prefix + assert_equal( + "http://www.singlefile.com/fantastic/settings/show", + @library_url.rewrite(:controller => "settings", :action => "show", :controller_prefix => "fantastic") + ) + end + + def test_path_parameters + assert_equal "http://www.singlefile.com/library/books/EXBC/0743536703/show", @library_url.rewrite(:path_params => {"type" => "EXBC"}) + end + + def test_parameters + assert_equal( + "http://www.singlefile.com/library/books/ISBN/0743536703/show?delete=1&name=David", + @library_url.rewrite(:params => {"delete" => "1", "name" => "David"}) + ) + end + + def test_parameters_with_id + @clean_urls.each do |url| + assert_equal( + "http://www.singlefile.com/identity/show?name=David&id=5", + url.rewrite( + :action => "show", + :params => { "id" => "5", "name" => "David" } + ) + ) + end + end + + def test_parameters_with_array + @clean_urls.each do |url| + assert_equal( + "http://www.singlefile.com/identity/show?id[]=3&id[]=5&id[]=10", + url.rewrite( + :action => "show", + :params => { 'id' => [ 3, 5, 10 ] } ) + ) + end + end + + def test_action_with_id + assert_equal( + "http://www.singlefile.com/identity/show/7", + @clean_url_with_id.rewrite( + :action => "show", + :id => 7 + ) + ) + @clean_urls.each do |url| + assert_equal( + "http://www.singlefile.com/identity/index/7", + url.rewrite(:id => 7) + ) + end + end + + def test_parameters_with_id_and_away + assert_equal( + "http://www.singlefile.com/identity/show/25?name=David", + @clean_url_with_id.rewrite( + :path_params => { "id" => "25" }, + :params => { "name" => "David" } + ) + ) + end + + def test_parameters_with_index_and_id + @clean_urls.each do |url| + assert_equal( + "http://www.singlefile.com/identity/index/25?name=David", + url.rewrite( + :path_params => { "id" => "25" }, + :params => { "name" => "David" } + ) + ) + end + end + + def test_action_going_away_from_id + assert_equal( + "http://www.singlefile.com/identity/list", + @clean_url_with_id.rewrite( + :action => "list" + ) + ) + end + + def test_parameters_with_direct_id_and_away + assert_equal( + "http://www.singlefile.com/identity/show/25?name=David", + @clean_url_with_id.rewrite( + :id => "25", + :params => { "name" => "David" } + ) + ) + end + + def test_parameters_with_direct_id_and_away + assert_equal( + "http://www.singlefile.com/store/open/25?name=David", + @clean_url_with_id.rewrite( + :controller => "store", + :action => "open", + :id => "25", + :params => { "name" => "David" } + ) + ) + end + + def test_parameters_to_id + @clean_urls.each do |url| + %w(show index).each do |action| + assert_equal( + "http://www.singlefile.com/identity/#{action}/25?name=David", + url.rewrite( + :action => action, + :path_params => { "id" => "25" }, + :params => { "name" => "David" } + ) + ) + end + end + end + + def test_parameters_from_id + assert_equal( + "http://www.singlefile.com/identity/", + @clean_url_with_id.rewrite( + :action => "index" + ) + ) + end + + def test_id_as_char_and_part_of_controller + assert_equal( + "http://www.singlefile.com/teachers/skill/5", + @clean_url_with_id_as_char.rewrite( + :action => "skill", + :id => 5 + ) + ) + end + + def test_from_clean_to_library + @clean_urls.each do |url| + assert_equal( + "http://www.singlefile.com/library/books/ISBN/0743536703/show?delete=1&name=David", + url.rewrite( + :controller_prefix => "library", + :controller => "books", + :action_prefix => "ISBN/0743536703", + :action => "show", + :params => { "delete" => "1", "name" => "David" } + ) + ) + end + end + + def test_from_library_to_clean + assert_equal( + "http://www.singlefile.com/identity/", + @library_url.rewrite( + :controller => "identity", :controller_prefix => "" + ) + ) + end + + def test_from_another_port + @library_url = ActionController::UrlRewriter.old_new(MockRequest.new( + "http://", + "www.singlefile.com", + 8080, + "/library/books/ISBN/0743536703/show", + { "type" => "ISBN", "code" => "0743536703" } + ), "books", "show") + + assert_equal( + "http://www.singlefile.com:8080/identity/", + @library_url.rewrite( + :controller => "identity", :controller_prefix => "" + ) + ) + end + + def test_basecamp + basecamp_url = ActionController::UrlRewriter.old_new(MockRequest.new( + "http://", + "projects.basecamp", + 80, + "/clients/disarray/1/msg/transcripts/", + {"category_name"=>"transcripts", "client_name"=>"disarray", "action"=>"index", "controller"=>"msg", "project_name"=>"1"} + ), "msg", "index") + + assert_equal( + "http://projects.basecamp/clients/disarray/1/msg/transcripts/1/comments", + basecamp_url.rewrite(:action_prefix => "transcripts/1", :action => "comments") + ) + end + + def test_on_explicit_index_page # My index page is very modest, thank you... + url = ActionController::UrlRewriter.old_new( + MockRequest.new( + "http://", "example.com", 80, "/controller/index", + {"controller"=>"controller", "action"=>"index"} + ), "controller", "index" + ) + assert_equal("http://example.com/controller/foo", url.rewrite(:action => 'foo')) + end + + def test_rewriting_on_similar_fragments + url = UrlMockFactory.create("/advertisements/advert/", {"controller"=>"advert", "action"=>"index"}) + assert_equal("http://example.com/advertisements/advert/news", url.rewrite(:action => 'news')) + end + + def test_rewriting_on_similar_fragments_with_action_prefixes + url = UrlMockFactory.create( + "/clients/prall/1/msg/all/", + { "category_name"=>"all", "client_name"=>"prall", "action"=>"index", "controller"=>"msg", "project_name"=>"1"} + ) + + assert_equal( + "http://example.com/clients/prall/1/msg/all/new", + url.rewrite({ :controller => "msg", :action_prefix => "all", :action => "new" }) + ) + + url = UrlMockFactory.create( + "/clients/prall/1/msg/all/", + { "category_name"=>"all", "client_name"=>"prall", "action"=>"index", "controller"=>"msg", "project_name"=>"1"} + ) + + assert_equal( + "http://example.com/clients/prall/1/msg/allous/new", + url.rewrite({ :controller => "msg", :action_prefix => "allous", :action => "new" }) + ) + end + + def test_clean_application_prefix + assert_equal "http://www.singlefile.com/namespace/library/books/ISBN/0743536703/show", + @library_url.rewrite(:application_prefix => "/namespace") + end + + def test_clean_application_prefix_with_controller_prefix + assert_equal "http://www.singlefile.com/namespace/shop/", + @library_url.rewrite(:application_prefix => "/namespace", + :controller_prefix => "shop" ) + end + + def test_blank_application_prefix + assert_equal "http://www.singlefile.com/library/books/ISBN/0743536703/show", + @library_url.rewrite(:application_prefix => "") + end + + def test_nil_application_prefix + assert_equal "http://www.singlefile.com/library/books/ISBN/0743536703/show", + @library_url.rewrite(:application_prefix => nil) + end +end diff --git a/actionpack/test/fixtures/fun/games/hello_world.rhtml b/actionpack/test/fixtures/fun/games/hello_world.rhtml new file mode 100644 index 0000000000..1ebfbe2539 --- /dev/null +++ b/actionpack/test/fixtures/fun/games/hello_world.rhtml @@ -0,0 +1 @@ +Living in a nested world \ No newline at end of file diff --git a/actionpack/test/fixtures/helpers/fun/games_helper.rb b/actionpack/test/fixtures/helpers/fun/games_helper.rb new file mode 100644 index 0000000000..bf60d9db0c --- /dev/null +++ b/actionpack/test/fixtures/helpers/fun/games_helper.rb @@ -0,0 +1,3 @@ +module Fun::GamesHelper + def stratego() "Iz guuut!" end +end \ No newline at end of file diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index 7a005ce970..cf9fc0a728 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -2,6 +2,10 @@ * Added IndifferentAccess as a way to wrap a hash by a symbol-based store that also can be accessed by string keys +* Added Inflector.constantize to turn "Admin::User" into a reference for the constant Admin::User + +* Added that Inflector.camelize and Inflector.underscore can deal with modules like turning "Admin::User" into "admin/user" and back + * Added Inflector.humanize to turn attribute names like employee_salary into "Employee salary". Used by automated error reporting in AR. * Added availability of class inheritable attributes to the masses #477 [bitsweat] diff --git a/activesupport/lib/core_ext/hash/indifferent_access.rb b/activesupport/lib/core_ext/hash/indifferent_access.rb index 3fe0999866..2353cfaf3b 100644 --- a/activesupport/lib/core_ext/hash/indifferent_access.rb +++ b/activesupport/lib/core_ext/hash/indifferent_access.rb @@ -8,17 +8,17 @@ class HashWithIndifferentAccess < Hash end end - alias_method :regular_read, :[] + alias_method :regular_reader, :[] unless method_defined?(:regular_reader) def [](key) case key - when Symbol: regular_read(key) || regular_read(key.to_s) - when String: regular_read(key) || regular_read(key.to_sym) - else regular_read(key) + when Symbol: regular_reader(key) || regular_reader(key.to_s) + when String: regular_reader(key) || regular_reader(key.to_sym) + else regular_reader(key) end end - alias_method :regular_writer, :[]= + alias_method :regular_writer, :[]= unless method_defined?(:regular_writer) def []=(key, value) regular_writer(key.is_a?(String) ? key.to_sym : key, value) diff --git a/activesupport/lib/core_ext/string/inflections.rb b/activesupport/lib/core_ext/string/inflections.rb index 5d1070b00a..aa4ff3a74d 100644 --- a/activesupport/lib/core_ext/string/inflections.rb +++ b/activesupport/lib/core_ext/string/inflections.rb @@ -39,6 +39,10 @@ module ActiveSupport #:nodoc: def foreign_key(separate_class_name_and_id_with_underscore = true) Inflector.foreign_key(self, separate_class_name_and_id_with_underscore) end + + def constantize + Inflector.constantize(self) + end end end end diff --git a/activesupport/lib/dependencies.rb b/activesupport/lib/dependencies.rb index 7f53998e7a..0a9b8e1d3d 100644 --- a/activesupport/lib/dependencies.rb +++ b/activesupport/lib/dependencies.rb @@ -2,7 +2,7 @@ require File.dirname(__FILE__) + '/module_attribute_accessors' module Dependencies extend self - + @@loaded = [ ] mattr_accessor :loaded @@ -41,6 +41,68 @@ module Dependencies def remove_subclasses_for(*classes) classes.each { |klass| klass.remove_subclasses } end + + # LoadingModules implement namespace-safe dynamic loading. + # They support automatic loading via const_missing, allowing contained items to be automatically + # loaded when required. No extra syntax is required, as expressions such as Controller::Admin::UserController + # load the relavent files automatically. + # + # Ruby-style modules are supported, as a folder named 'submodule' will load 'submodule.rb' when available. + class LoadingModule < Module + attr_reader :path + + def initialize(filesystem_root, path=[]) + @path = path + @filesystem_root = filesystem_root + end + + # The path to this module in the filesystem. + # Any subpath provided is taken to be composed of filesystem names. + def filesystem_path(subpath=[]) + File.join(@filesystem_root, self.path, subpath) + end + + # Load missing constants if possible. + def const_missing(name) + return const_get(name) if const_defined?(name) == false && const_load!(name) + super(name) + end + + # Load the controller class or a parent module. + def const_load!(name) + name = name.to_s if name.kind_of? Symbol + + if File.directory? filesystem_path(name.underscore) + # Is it a submodule? If so, create a new LoadingModule *before* loading it. + # This ensures that subitems will be loadable + new_module = LoadingModule.new(@filesystem_root, self.path + [name.underscore]) + const_set(name, new_module) + Object.const_set(name, new_module) if @path.empty? + end + + source_file = filesystem_path("#{(name == 'ApplicationController' ? 'Application' : name).underscore}.rb") + self.load_file(source_file) if File.file?(source_file) + self.const_defined?(name.camelize) + end + + # Is this name present or loadable? + # This method is used by Routes to find valid controllers. + def const_available?(name) + name = name.to_s unless name.kind_of? String + File.directory?(filesystem_path(name.underscore)) || File.file?(filesystem_path("#{name.underscore}.rb")) + end + + def clear + constants.each do |name| + Object.send(:remove_const, name) if Object.const_defined?(name) && @path.empty? + self.send(:remove_const, name) + end + end + + def load_file(file_path) + Controllers.module_eval(IO.read(file_path), file_path, 1) # Hard coded Controller line here!!! + end + end end Object.send(:define_method, :require_or_load) { |file_name| Dependencies.require_or_load(file_name) } unless Object.respond_to?(:require_or_load) @@ -52,6 +114,9 @@ class Object #:nodoc: # Use const_missing to autoload associations so we don't have to # require_association when using single-table inheritance. def const_missing(class_id) + if Object.const_defined?(:Controllers) and Object::Controllers.const_available?(class_id) + return Object::Controllers.const_get(class_id) + end begin require_or_load(class_id.to_s.demodulize.underscore) if Object.const_defined?(class_id) then return Object.const_get(class_id) else raise LoadError end diff --git a/activesupport/lib/inflector.rb b/activesupport/lib/inflector.rb index 065fad3762..a9b2f87be4 100644 --- a/activesupport/lib/inflector.rb +++ b/activesupport/lib/inflector.rb @@ -20,11 +20,11 @@ module Inflector end def camelize(lower_case_and_underscored_word) - lower_case_and_underscored_word.to_s.gsub(/(^|_)(.)/){$2.upcase} + lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase } end def underscore(camel_cased_word) - camel_cased_word.to_s.gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase + camel_cased_word.to_s.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase end def humanize(lower_case_and_underscored_word) @@ -47,7 +47,13 @@ module Inflector Inflector.underscore(Inflector.demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id") end - + + def constantize(camel_cased_word) + camel_cased_word.split("::").inject(Object) do |final_type, part| + final_type = final_type.const_get(part) + end + end + private def plural_rules #:doc: [ diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 4523430ebe..cdab0f9ed0 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -1,6 +1,13 @@ require 'test/unit' require File.dirname(__FILE__) + '/../lib/inflector' +module A + module B + class C + end + end +end + class InflectorTest < Test::Unit::TestCase SingularToPlural = { "search" => "searches", @@ -50,6 +57,12 @@ class InflectorTest < Test::Unit::TestCase "SpecialGuest" => "special_guest", "ApplicationController" => "application_controller" } + + CamelWithModuleToUnderscoreWithSlash = { + "Admin::Product" => "admin/product", + "Users::Commission::Department" => "users/commission/department", + "UsersSection::CommissionDepartment" => "users_section/commission_department", + } ClassNameToForeignKeyWithUnderscore = { "Person" => "person_id", @@ -100,6 +113,18 @@ class InflectorTest < Test::Unit::TestCase assert_equal "html_tidy_generator", Inflector.underscore("HTMLTidyGenerator") end + def test_camelize_with_module + CamelWithModuleToUnderscoreWithSlash.each do |camel, underscore| + assert_equal(camel, Inflector.camelize(underscore)) + end + end + + def test_underscore_with_slashes + CamelWithModuleToUnderscoreWithSlash.each do |camel, underscore| + assert_equal(underscore, Inflector.underscore(camel)) + end + end + def test_demodulize assert_equal "Account", Inflector.demodulize("MyApplication::Billing::Account") end @@ -131,4 +156,10 @@ class InflectorTest < Test::Unit::TestCase assert_equal(human, Inflector.humanize(underscore)) end end + + def test_constantize + assert_equal A::B::C, Inflector.constantize("A::B::C") + assert_equal InflectorTest, Inflector.constantize("InflectorTest") + assert_raises(NameError) { Inflector.constantize("UnknownClass") } + end end \ No newline at end of file diff --git a/activesupport/test/loading_module/admin/access_controller.rb b/activesupport/test/loading_module/admin/access_controller.rb new file mode 100644 index 0000000000..ddcbda8132 --- /dev/null +++ b/activesupport/test/loading_module/admin/access_controller.rb @@ -0,0 +1,2 @@ +class Admin::AccessController +end diff --git a/activesupport/test/loading_module/admin/user_controller.rb b/activesupport/test/loading_module/admin/user_controller.rb new file mode 100644 index 0000000000..f265f1597a --- /dev/null +++ b/activesupport/test/loading_module/admin/user_controller.rb @@ -0,0 +1,2 @@ +class Admin::UserController +end diff --git a/activesupport/test/loading_module/content_controller.rb b/activesupport/test/loading_module/content_controller.rb new file mode 100644 index 0000000000..f0870161e5 --- /dev/null +++ b/activesupport/test/loading_module/content_controller.rb @@ -0,0 +1,2 @@ +class ContentController +end diff --git a/activesupport/test/loading_module/resource_controller.rb b/activesupport/test/loading_module/resource_controller.rb new file mode 100644 index 0000000000..d948f366bf --- /dev/null +++ b/activesupport/test/loading_module/resource_controller.rb @@ -0,0 +1,2 @@ +class ResourceController +end diff --git a/activesupport/test/loading_module_tests.rb b/activesupport/test/loading_module_tests.rb new file mode 100644 index 0000000000..c1d8c7de62 --- /dev/null +++ b/activesupport/test/loading_module_tests.rb @@ -0,0 +1,63 @@ +require 'test/unit' +require '../lib/core_ext.rb' +require '../lib/dependencies.rb' + +STAGING_DIRECTORY = 'loading_module' + +class LoadingModuleTests < Test::Unit::TestCase + def setup + @loading_module = Dependencies::LoadingModule.new(STAGING_DIRECTORY) + Object.const_set(:Controllers, @loading_module) + end + def teardown + @loading_module.clear + Object.send :remove_const, :Controllers + end + + def test_setup + assert_kind_of Dependencies::LoadingModule, @loading_module + end + + def test_const_available + assert @loading_module.const_available?(:Admin) + assert @loading_module.const_available?(:ResourceController) + assert @loading_module.const_available?(:ContentController) + assert @loading_module.const_available?("ContentController") + + assert_equal false, @loading_module.const_available?(:AdminController) + assert_equal false, @loading_module.const_available?(:RandomName) + end + + def test_const_load_module + assert @loading_module.const_load!(:Admin) + assert_kind_of Module, @loading_module::Admin + assert_kind_of Dependencies::LoadingModule, @loading_module::Admin + end + + def test_const_load_controller + assert @loading_module.const_load!(:ContentController) + assert_kind_of Class, @loading_module::ContentController + end + + def test_const_load_nested_controller + assert @loading_module.const_load!(:Admin) + assert @loading_module::Admin.const_available?(:UserController) + assert @loading_module::Admin.const_load!(:UserController) + assert_kind_of Class, @loading_module::Admin::UserController + end + + def test_pretty_access + assert_kind_of Module, @loading_module::Admin + assert_kind_of Dependencies::LoadingModule, @loading_module::Admin + + assert_kind_of Class, @loading_module::Admin::UserController + assert_kind_of Class, @loading_module::Admin::AccessController + assert_kind_of Class, @loading_module::ResourceController + assert_kind_of Class, @loading_module::ContentController + end + + def test_missing_name + assert_raises(NameError) {@loading_module::PersonController} + assert_raises(NameError) {@loading_module::Admin::FishController} + end +end \ No newline at end of file diff --git a/railties/Rakefile b/railties/Rakefile index ba795ea2b4..2cd1086a5a 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -132,6 +132,7 @@ end task :copy_configs do cp "configs/database.yml", "#{PKG_DESTINATION}/config/database.yml" + cp "configs/routes.rb", "#{PKG_DESTINATION}/config/routes.rb" cp "configs/apache.conf", "#{PKG_DESTINATION}/public/.htaccess" diff --git a/railties/bin/destroy b/railties/bin/destroy index 6c1848ce1e..ba6dc7703f 100644 --- a/railties/bin/destroy +++ b/railties/bin/destroy @@ -1,8 +1,5 @@ #!/usr/local/bin/ruby require File.dirname(__FILE__) + '/../config/environment' require 'rails_generator' -require 'rails_generator/simple_logger' require 'rails_generator/scripts/destroy' - -Rails::Generator::Base.logger = Rails::Generator::SimpleLogger.new(STDOUT) Rails::Generator::Scripts::Destroy.new.run(ARGV) diff --git a/railties/bin/generate b/railties/bin/generate index 8bce002510..dde69e61a1 100755 --- a/railties/bin/generate +++ b/railties/bin/generate @@ -1,8 +1,5 @@ #!/usr/local/bin/ruby require File.dirname(__FILE__) + '/../config/environment' require 'rails_generator' -require 'rails_generator/simple_logger' require 'rails_generator/scripts/generate' - -Rails::Generator::Base.logger = Rails::Generator::SimpleLogger.new(STDOUT) Rails::Generator::Scripts::Generate.new.run(ARGV) diff --git a/railties/bin/rails b/railties/bin/rails index da9b2af041..3d23b99609 100755 --- a/railties/bin/rails +++ b/railties/bin/rails @@ -1,7 +1,4 @@ require File.dirname(__FILE__) + '/../lib/rails_generator' -require 'rails_generator/simple_logger' require 'rails_generator/scripts/generate' - -Rails::Generator::Base.logger = Rails::Generator::SimpleLogger.new(STDOUT) Rails::Generator::Base.use_application_sources! Rails::Generator::Scripts::Generate.new.run(ARGV, :generator => 'app') diff --git a/railties/bin/update b/railties/bin/update index 64cc49cf02..430d325b07 100644 --- a/railties/bin/update +++ b/railties/bin/update @@ -1,8 +1,5 @@ #!/usr/local/bin/ruby require File.dirname(__FILE__) + '/../config/environment' require 'rails_generator' -require 'rails_generator/simple_logger' require 'rails_generator/scripts/update' - -Rails::Generator::Base.logger = Rails::Generator::SimpleLogger.new(STDOUT) Rails::Generator::Scripts::Update.new.run(ARGV) diff --git a/railties/configs/apache.conf b/railties/configs/apache.conf index 5548292f8d..0b2f4cd99e 100755 --- a/railties/configs/apache.conf +++ b/railties/configs/apache.conf @@ -1,61 +1,5 @@ -# General Apache options -AddHandler fastcgi-script .fcgi -AddHandler cgi-script .cgi -Options +FollowSymLinks +ExecCGI - -# Make sure that mod_ruby.c has been added and loaded as a module with Apache RewriteEngine On +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^(.*)$ /dispatch.fcgi?$1 [QSA,L] -# Change extension from .cgi to .fcgi to switch to FCGI and to .rb to switch to mod_ruby -RewriteBase /dispatch.cgi - -# Enable this rewrite rule to point to the controller/action that should serve root. -# RewriteRule ^$ /controller/action [R] - -#