aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2006-07-31 18:59:58 +0000
committerDavid Heinemeier Hansson <david@loudthinking.com>2006-07-31 18:59:58 +0000
commit865b175765edf6138deb6f10ae3280ddea4cc7fd (patch)
tree240d292136fbd7dc82b3e3652e58ff870f9668f1
parentffaecb792e8f23efba513ffd8655f7832ba67b89 (diff)
downloadrails-865b175765edf6138deb6f10ae3280ddea4cc7fd.tar.gz
rails-865b175765edf6138deb6f10ae3280ddea4cc7fd.tar.bz2
rails-865b175765edf6138deb6f10ae3280ddea4cc7fd.zip
Added map.resources from the Simply Restful plugin (backwards incompatible with the plugin!) [DHH]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4637 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
-rw-r--r--actionpack/CHANGELOG6
-rwxr-xr-xactionpack/lib/action_controller/base.rb1
-rwxr-xr-xactionpack/lib/action_controller/request.rb4
-rw-r--r--actionpack/lib/action_controller/resources.rb170
-rw-r--r--actionpack/lib/action_controller/routing.rb9
-rw-r--r--actionpack/test/controller/resources_test.rb109
6 files changed, 290 insertions, 9 deletions
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG
index 7f6134d455..40ff1a5939 100644
--- a/actionpack/CHANGELOG
+++ b/actionpack/CHANGELOG
@@ -1,5 +1,11 @@
*SVN*
+* Added map.resources from the Simply Restful plugin [DHH]. Examples (the API has changed to use plurals!):
+
+ map.resources :messages
+ map.resources :messages, :comments
+ map.resources :messages, :new => { :preview => :post }
+
* Fixed that integration simulation of XHRs should set Accept header as well [Edward Frederick]
* TestRequest#reset_session should restore a TestSession, not a hash [Koz]
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index fb0a7b93d2..fd284ae72b 100755
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -2,6 +2,7 @@ require 'action_controller/mime_type'
require 'action_controller/request'
require 'action_controller/response'
require 'action_controller/routing'
+require 'action_controller/resources'
require 'action_controller/url_rewriter'
require 'drb'
require 'set'
diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb
index 86547919b9..0802353405 100755
--- a/actionpack/lib/action_controller/request.rb
+++ b/actionpack/lib/action_controller/request.rb
@@ -15,7 +15,9 @@ module ActionController
# Returns the HTTP request method as a lowercase symbol (:get, for example)
def method
- @request_method ||= @env['REQUEST_METHOD'].downcase.to_sym
+ @request_method ||= (method = parameters[:_method] && method == :post) ?
+ method.to_s.downcase.to_sym :
+ @env['REQUEST_METHOD'].downcase.to_sym
end
# Is this a GET request? Equivalent to request.method == :get
diff --git a/actionpack/lib/action_controller/resources.rb b/actionpack/lib/action_controller/resources.rb
new file mode 100644
index 0000000000..fb27759004
--- /dev/null
+++ b/actionpack/lib/action_controller/resources.rb
@@ -0,0 +1,170 @@
+module ActionController
+ module Resources
+ class Resource #:nodoc:
+ attr_reader :collection_methods, :member_methods, :new_methods
+ attr_reader :path_prefix, :name_prefix
+ attr_reader :plural, :singular
+ attr_reader :options
+
+ def initialize(entities, options)
+ @plural = entities
+ @singular = options[:singular] || plural.to_s.singularize
+
+ @options = options
+
+ arrange_actions
+ add_default_actions
+ set_prefixes
+ end
+
+ def controller
+ (options[:controller] || plural).to_s
+ end
+
+ def path
+ "#{path_prefix}/#{plural}"
+ end
+
+ def new_path
+ "#{path}/new"
+ end
+
+ def member_path
+ "#{path}/:id"
+ end
+
+ def nesting_path_prefix
+ "#{path_prefix}/#{plural}/:#{singular}_id"
+ end
+
+ private
+ def arrange_actions
+ @collection_methods = arrange_actions_by_methods(options.delete(:collection))
+ @member_methods = arrange_actions_by_methods(options.delete(:member))
+ @new_methods = arrange_actions_by_methods(options.delete(:new))
+ end
+
+ def add_default_actions
+ add_default_action(collection_methods, :post, :create)
+ add_default_action(member_methods, :get, :edit)
+ add_default_action(member_methods, :put, :update)
+ add_default_action(member_methods, :delete, :destroy)
+ add_default_action(new_methods, :get, :new)
+ end
+
+ def set_prefixes
+ @path_prefix = options.delete(:path_prefix)
+ @name_prefix = options.delete(:name_prefix)
+ end
+
+ def arrange_actions_by_methods(actions)
+ arrayize_values(flip_keys_and_values(actions || {}))
+ end
+
+ def add_default_action(collection, method, action)
+ (collection[method] ||= []).unshift(action)
+ end
+
+ def flip_keys_and_values(hash)
+ hash.inject({}) do |flipped_hash, (key, value)|
+ flipped_hash[value] = key
+ flipped_hash
+ end
+ end
+
+ def arrayize_values(hash)
+ hash.each do |(key, value)|
+ unless value.is_a?(Array)
+ hash[key] = []
+ hash[key] << value
+ end
+ end
+ end
+ end
+
+ def resources(*entities)
+ options = entities.last.is_a?(Hash) ? entities.pop : { }
+ entities.each { |entity| map_resource(entity, options) { yield if block_given? } }
+ end
+
+ private
+ def map_resource(entities, options = {}, &block)
+ resource = Resource.new(entities, options)
+
+ with_options :controller => resource.controller do |map|
+ map_collection_actions(map, resource)
+ map_new_actions(map, resource)
+ map_member_actions(map, resource)
+
+ if block_given?
+ with_options(:path_prefix => resource.nesting_path_prefix, &block)
+ end
+ end
+ end
+
+ def map_collection_actions(map, resource)
+ resource.collection_methods.each do |method, actions|
+ primary = actions.shift.to_s if method != :get
+ route_options = requirements_for(method)
+
+ actions.each do |action|
+ map.named_route(
+ "#{resource.name_prefix}#{action}_#{resource.plural}",
+ "#{resource.path};#{action}",
+ route_options.merge(:action => action.to_s)
+ )
+
+ map.named_route(
+ "formatted_#{resource.name_prefix}#{action}_#{resource.plural}",
+ "#{resource.path}.:format;#{action}",
+ route_options.merge(:action => action.to_s)
+ )
+ end
+
+ unless primary.blank?
+ map.connect(resource.path, route_options.merge(:action => primary))
+ map.connect("#{resource.path}.:format", route_options.merge(:action => primary))
+ end
+
+ map.named_route("#{resource.name_prefix}#{resource.plural}", resource.path, :action => "index", :conditions => { :method => :get })
+ map.named_route("formatted_#{resource.name_prefix}#{resource.plural}", "#{resource.path}.:format", :action => "index", :conditions => { :method => :get })
+ end
+ end
+
+ def map_new_actions(map, resource)
+ resource.new_methods.each do |method, actions|
+ route_options = requirements_for(method)
+ actions.each do |action|
+ path = action == :new ? resource.new_path : "#{resource.new_path};#{action}"
+ name = "new_#{resource.plural}"
+ name = "#{action}_#{name}" unless action == :new
+
+ map.named_route("#{resource.name_prefix}#{name}", path, route_options.merge(:action => action.to_s))
+ map.named_route("formatted_#{resource.name_prefix}#{name}", action == :new ? "#{resource.new_path}.:format" : "#{resource.new_path}.:format;#{action}", route_options.merge(:action => action.to_s))
+ end
+ end
+ end
+
+ def map_member_actions(map, resource)
+ resource.member_methods.each do |method, actions|
+ route_options = requirements_for(method)
+ primary = actions.shift.to_s unless [ :get, :post, :any ].include?(method)
+
+ actions.each do |action|
+ map.named_route("#{resource.name_prefix}#{action}_#{resource.singular}", "#{resource.member_path};#{action}", route_options.merge(:action => action.to_s))
+ map.named_route("formatted_#{resource.name_prefix}#{action}_#{resource.singular}", "#{resource.member_path}.:format;#{action}", route_options.merge(:action => action.to_s))
+ end
+
+ map.connect(resource.member_path, route_options.merge(:action => primary)) unless primary.blank?
+ map.named_route("#{resource.name_prefix}#{resource.singular}", resource.member_path, :action => "show", :conditions => { :method => :get })
+ map.named_route("formatted_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}.:format", :action => "show", :conditions => { :method => :get })
+ end
+ end
+
+ def requirements_for(method)
+ method == :any ? {} : { :conditions => { :method => method } }
+ end
+ end
+end
+
+ActionController::Routing::RouteSet::Mapper.send :include, ActionController::Resources \ No newline at end of file
diff --git a/actionpack/lib/action_controller/routing.rb b/actionpack/lib/action_controller/routing.rb
index 835758675e..7ca773469c 100644
--- a/actionpack/lib/action_controller/routing.rb
+++ b/actionpack/lib/action_controller/routing.rb
@@ -58,7 +58,7 @@ module ActionController
use_controllers! nil
end
- def normalize_paths(paths=$LOAD_PATH)
+ def normalize_paths(paths = $LOAD_PATH)
# do the hokey-pokey of path normalization...
paths = paths.collect do |path|
path = path.
@@ -351,7 +351,6 @@ module ActionController
end
protected
-
def requirement_for(key)
return requirements[key] if requirements.key? key
segments.each do |segment|
@@ -415,7 +414,6 @@ module ActionController
def optionality_implied?
false
end
-
end
class StaticSegment < Segment
@@ -450,11 +448,9 @@ module ActionController
def to_s
value
end
-
end
class DividerSegment < StaticSegment
-
def initialize(value = nil)
super(value)
self.raw = true
@@ -464,7 +460,6 @@ module ActionController
def optionality_implied?
true
end
-
end
class DynamicSegment < Segment
@@ -760,7 +755,6 @@ module ActionController
end
class RouteSet
-
# Mapper instances are used to build routes. The object passed to the draw
# block in config/routes.rb is a Mapper instance.
#
@@ -839,7 +833,6 @@ module ActionController
end
private
-
def url_helper_name(name, kind = :url)
:"#{name}_#{kind}"
end
diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb
new file mode 100644
index 0000000000..ff90f8d742
--- /dev/null
+++ b/actionpack/test/controller/resources_test.rb
@@ -0,0 +1,109 @@
+require File.dirname(__FILE__) + '/../abstract_unit'
+
+class MessagesController < ActionController::Base
+ def rescue_action(e) raise e end
+end
+
+class CommentsController < ActionController::Base
+ def rescue_action(e) raise e end
+end
+
+class ResourcesTest < Test::Unit::TestCase
+ def test_default_restful_routes
+ with_restful_routing :messages do
+ assert_restful_routes_for :messages do
+ routing_options = {:controller => '/messages'}
+ end
+ end
+ end
+
+ def test_with_path_prefix
+ with_restful_routing :messages, :path_prefix => '/thread/:thread_id' do
+ assert_restful_routes_for :messages, :path_prefix => 'thread/5/', :options => { :thread_id => '5' }
+ end
+ end
+
+ def test_with_collection_action
+ with_restful_routing :messages, :collection => { :rss => :get } do
+ assert_restful_routes_for :messages do |options|
+ assert_routing "/messages;rss", options.merge(:action => 'rss')
+ end
+ end
+ end
+
+ def test_with_member_action
+ [:put, :post].each do |method|
+ with_restful_routing :messages, :member => { :mark => method } do
+ assert_restful_routes_for :messages do |options|
+ assert_recognizes(
+ options.merge(:action => 'mark', :id => '1'),
+ {:path => "/messages/1;mark", :method => method})
+ end
+ end
+ end
+ end
+
+ def test_with_new_action
+ with_restful_routing :messages, :new => { :preview => :post } do
+ assert_restful_routes_for :messages do |options|
+ assert_recognizes(
+ options.merge(:action => 'preview'),
+ {:path => "/messages/new;preview", :method => :post})
+ end
+ end
+ end
+
+ def xtest_nested_restful_routes
+ with_routing do |set|
+ set.draw do |map|
+ map.resources(:messages) do
+ map.resources(:comments)
+ end
+ end
+
+ with_options({ :controller => :comments }) do |controller|
+ controller.assert_routing "/messages/1/comments", :action => 'index'
+ controller.assert_routing "/messages/1/comments.xml" , :action => 'index', :format => 'xml'
+ controller.assert_routing "/messages/1/comments/new", :action => 'new'
+ controller.assert_routing "/messages/1/comments/1", :action => 'show', :id => '1'
+ controller.assert_routing "/messages/1/comments/1;edit", :action => 'edit', :id => '1'
+ controller.assert_routing "/messages/1/comments/1.xml", :action => 'show', :id => '1', :format => 'xml'
+ end
+ end
+ end
+
+ protected
+ def with_restful_routing(resource, *args)
+ with_routing do |set|
+ set.draw { |map| map.resources(resource, *args) }
+ yield
+ end
+ end
+
+ def assert_restful_routes_for(controller_name, options = {})
+ (options[:options] ||= {})[:controller] = controller_name.to_s
+
+ with_options(options[:options]) do |controller|
+ controller.assert_routing "/#{options[:path_prefix]}#{controller_name}", :action => 'index'
+ controller.assert_routing "/#{options[:path_prefix]}#{controller_name}.xml" , :action => 'index', :format => 'xml'
+ controller.assert_routing "/#{options[:path_prefix]}#{controller_name}/new", :action => 'new'
+ controller.assert_routing "/#{options[:path_prefix]}#{controller_name}/1", :action => 'show', :id => '1'
+ controller.assert_routing "/#{options[:path_prefix]}#{controller_name}/1;edit", :action => 'edit', :id => '1'
+ controller.assert_routing "/#{options[:path_prefix]}#{controller_name}/1.xml", :action => 'show', :id => '1', :format => 'xml'
+ end
+
+ assert_recognizes(
+ options[:options].merge(:action => 'create'),
+ {:path => "/#{options[:path_prefix]}#{controller_name}", :method => :post})
+
+ assert_recognizes(
+ options[:options].merge(:action => 'update', :id => '1'),
+ {:path => "/#{options[:path_prefix]}#{controller_name}/1", :method => :put})
+
+ assert_recognizes(
+ options[:options].merge(:action => 'destroy', :id => '1'),
+ {:path => "/#{options[:path_prefix]}#{controller_name}/1", :method => :delete})
+
+ yield options[:options] if block_given?
+ end
+end