From 865b175765edf6138deb6f10ae3280ddea4cc7fd Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 31 Jul 2006 18:59:58 +0000 Subject: 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 --- actionpack/CHANGELOG | 6 + actionpack/lib/action_controller/base.rb | 1 + actionpack/lib/action_controller/request.rb | 4 +- actionpack/lib/action_controller/resources.rb | 170 ++++++++++++++++++++++++++ actionpack/lib/action_controller/routing.rb | 9 +- actionpack/test/controller/resources_test.rb | 109 +++++++++++++++++ 6 files changed, 290 insertions(+), 9 deletions(-) create mode 100644 actionpack/lib/action_controller/resources.rb create mode 100644 actionpack/test/controller/resources_test.rb (limited to 'actionpack') 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 -- cgit v1.2.3