mirror of
https://github.com/github/rails.git
synced 2026-01-30 16:58:15 -05:00
Add singleton resources. [Rick Olson]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@5701 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
*SVN*
|
||||
|
||||
* Add singleton resources. [Rick Olson]
|
||||
|
||||
map.resource :account
|
||||
|
||||
GET /account
|
||||
GET /account;edit
|
||||
UPDATE /account
|
||||
DELEE /account
|
||||
|
||||
* respond_to recognizes JSON. render :json => @person.to_json automatically sets the content type and takes a :callback option to specify a client-side function to call using the rendered JSON as an argument. #4185 [Scott Raymond, eventualbuddha]
|
||||
# application/json response with body 'Element.show({:name: "David"})'
|
||||
respond_to do |format|
|
||||
|
||||
@@ -18,26 +18,26 @@ module ActionController
|
||||
end
|
||||
|
||||
def controller
|
||||
(options[:controller] || plural).to_s
|
||||
@controller ||= (options[:controller] || plural).to_s
|
||||
end
|
||||
|
||||
def path
|
||||
"#{path_prefix}/#{plural}"
|
||||
@path ||= "#{path_prefix}/#{plural}"
|
||||
end
|
||||
|
||||
def new_path
|
||||
"#{path}/new"
|
||||
@new_path ||= "#{path}/new"
|
||||
end
|
||||
|
||||
def member_path
|
||||
"#{path}/:id"
|
||||
@member_path ||= "#{path}/:id"
|
||||
end
|
||||
|
||||
def nesting_path_prefix
|
||||
"#{path}/:#{singular}_id"
|
||||
@nesting_path_prefix ||= "#{path}/:#{singular}_id"
|
||||
end
|
||||
|
||||
private
|
||||
protected
|
||||
def arrange_actions
|
||||
@collection_methods = arrange_actions_by_methods(options.delete(:collection))
|
||||
@member_methods = arrange_actions_by_methods(options.delete(:member))
|
||||
@@ -65,7 +65,34 @@ module ActionController
|
||||
(collection[method] ||= []).unshift(action)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
class SingletonResource < Resource #:nodoc:
|
||||
def initialize(entity, options)
|
||||
@singular = entity
|
||||
@plural = options[:plural] || singular.to_s.pluralize
|
||||
|
||||
@options = options
|
||||
arrange_actions
|
||||
add_default_actions
|
||||
set_prefixes
|
||||
end
|
||||
|
||||
def controller
|
||||
@controller ||= (options[:controller] || singular).to_s
|
||||
end
|
||||
|
||||
def path
|
||||
@path ||= "#{path_prefix}/#{singular}"
|
||||
end
|
||||
|
||||
def new_path
|
||||
nil
|
||||
end
|
||||
|
||||
alias_method :member_path, :path
|
||||
alias_method :nesting_path_prefix, :path
|
||||
end
|
||||
|
||||
# Creates named routes for implementing verb-oriented controllers. This is
|
||||
# useful for implementing REST API's, where a single resource has different
|
||||
# behavior based on the HTTP verb (method) used to access it.
|
||||
@@ -209,6 +236,55 @@ module ActionController
|
||||
entities.each { |entity| map_resource entity, options.dup, &block }
|
||||
end
|
||||
|
||||
# Creates named routes for implementing verb-oriented controllers for a singleton resource.
|
||||
# A singleton resource is global to the current user visiting the application, such as a user's
|
||||
# /account profile.
|
||||
#
|
||||
# See map.resources for general conventions. These are the main differences:
|
||||
# - a singular name is given to map.resource. The default controller name is taken from the singular name.
|
||||
# - To specify a custom plural name, use the :plural option. There is no :singular option
|
||||
# - No default index, new, or create routes are created for the singleton resource controller.
|
||||
# - When nesting singleton resources, only the singular name is used as the path prefix (example: 'account/messages/1')
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# map.resource :account
|
||||
#
|
||||
# class AccountController < ActionController::Base
|
||||
# # GET account_url
|
||||
# def show
|
||||
# # find and return a specific message
|
||||
# end
|
||||
#
|
||||
# # GET edit_account_url
|
||||
# def edit
|
||||
# # return an HTML form for editing a specific message
|
||||
# end
|
||||
#
|
||||
# # PUT account_url
|
||||
# def update
|
||||
# # find and update a specific message
|
||||
# end
|
||||
#
|
||||
# # DELETE account_url
|
||||
# def destroy
|
||||
# # delete a specific message
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Along with the routes themselves, #resource generates named routes for use in
|
||||
# controllers and views. <tt>map.resource :account</tt> produces the following named routes and helpers:
|
||||
#
|
||||
# Named Route Helpers
|
||||
# account account_url(id), hash_for_account_url(id),
|
||||
# account_path(id), hash_for_account_path(id)
|
||||
# edit_account edit_account_url(id), hash_for_edit_account_url(id),
|
||||
# edit_account_path(id), hash_for_edit_account_path(id)
|
||||
def resource(*entities, &block)
|
||||
options = entities.last.is_a?(Hash) ? entities.pop : { }
|
||||
entities.each { |entity| map_singleton_resource entity, options.dup, &block }
|
||||
end
|
||||
|
||||
private
|
||||
def map_resource(entities, options = {}, &block)
|
||||
resource = Resource.new(entities, options)
|
||||
@@ -224,6 +300,18 @@ module ActionController
|
||||
end
|
||||
end
|
||||
|
||||
def map_singleton_resource(entities, options = {}, &block)
|
||||
resource = SingletonResource.new(entities, options)
|
||||
|
||||
with_options :controller => resource.controller do |map|
|
||||
map_member_actions(map, resource)
|
||||
|
||||
if block_given?
|
||||
with_options(:path_prefix => resource.singular, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def map_collection_actions(map, resource)
|
||||
resource.collection_methods.each do |method, actions|
|
||||
route_options = requirements_for(method)
|
||||
@@ -284,7 +372,7 @@ module ActionController
|
||||
map.connect(resource.member_path, :action => "destroy", :conditions => { :method => :delete })
|
||||
map.connect("#{resource.member_path}.:format", :action => "destroy", :conditions => { :method => :delete })
|
||||
end
|
||||
|
||||
|
||||
def requirements_for(method)
|
||||
method == :any ? {} : { :conditions => { :method => method } }
|
||||
end
|
||||
|
||||
@@ -2,6 +2,7 @@ require File.dirname(__FILE__) + '/../abstract_unit'
|
||||
|
||||
class ResourcesController < ActionController::Base
|
||||
def index() render :nothing => true end
|
||||
alias_method :show, :index
|
||||
def rescue_action(e) raise e end
|
||||
end
|
||||
|
||||
@@ -9,6 +10,8 @@ class ThreadsController < ResourcesController; end
|
||||
class MessagesController < ResourcesController; end
|
||||
class CommentsController < ResourcesController; end
|
||||
|
||||
class AccountController < ResourcesController; end
|
||||
class AdminController < ResourcesController; end
|
||||
|
||||
class ResourcesTest < Test::Unit::TestCase
|
||||
def test_should_arrange_actions
|
||||
@@ -172,6 +175,66 @@ class ResourcesTest < Test::Unit::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_create_singleton_resource_routes
|
||||
with_singleton_resources :account do
|
||||
assert_singleton_restful_for :account
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_create_multiple_singleton_resource_routes
|
||||
with_singleton_resources :account, :admin do
|
||||
assert_singleton_restful_for :account
|
||||
assert_singleton_restful_for :admin
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_create_nested_singleton_resource_routes
|
||||
with_routing do |set|
|
||||
set.draw do |map|
|
||||
map.resource :admin do |admin|
|
||||
admin.resource :account
|
||||
end
|
||||
end
|
||||
|
||||
assert_singleton_restful_for :admin
|
||||
assert_singleton_restful_for :account, :path_prefix => 'admin/'
|
||||
end
|
||||
end
|
||||
|
||||
def test_singleton_resource_with_member_action
|
||||
[:put, :post].each do |method|
|
||||
with_singleton_resources :account, :member => { :reset => method } do
|
||||
reset_options = {:action => 'reset'}
|
||||
reset_path = "/account;reset"
|
||||
assert_singleton_routes_for :account do |options|
|
||||
assert_recognizes(options.merge(reset_options), :path => reset_path, :method => method)
|
||||
end
|
||||
|
||||
assert_singleton_named_routes_for :account do |options|
|
||||
assert_named_route reset_path, :reset_account_path, reset_options
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_singleton_resource_with_two_member_actions_with_same_method
|
||||
[:put, :post].each do |method|
|
||||
with_singleton_resources :account, :member => { :reset => method, :disable => method } do
|
||||
%w(reset disable).each do |action|
|
||||
action_options = {:action => action}
|
||||
action_path = "/account;#{action}"
|
||||
assert_singleton_routes_for :account do |options|
|
||||
assert_recognizes(options.merge(action_options), :path => action_path, :method => method)
|
||||
end
|
||||
|
||||
assert_singleton_named_routes_for :account do |options|
|
||||
assert_named_route action_path, "#{action}_account_path".to_sym, action_options
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def with_restful_routing(*args)
|
||||
with_routing do |set|
|
||||
@@ -179,6 +242,13 @@ class ResourcesTest < Test::Unit::TestCase
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def with_singleton_resources(*args)
|
||||
with_routing do |set|
|
||||
set.draw { |map| map.resource(*args) }
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# runs assert_restful_routes_for and assert_restful_named_routes for on the controller_name and options, without passing a block.
|
||||
def assert_simply_restful_for(controller_name, options = {})
|
||||
@@ -186,6 +256,11 @@ class ResourcesTest < Test::Unit::TestCase
|
||||
assert_restful_named_routes_for controller_name, options
|
||||
end
|
||||
|
||||
def assert_singleton_restful_for(singleton_name, options = {})
|
||||
assert_singleton_routes_for singleton_name, options
|
||||
assert_singleton_named_routes_for singleton_name, options
|
||||
end
|
||||
|
||||
def assert_restful_routes_for(controller_name, options = {})
|
||||
(options[:options] ||= {})[:controller] = controller_name.to_s
|
||||
|
||||
@@ -242,6 +317,35 @@ class ResourcesTest < Test::Unit::TestCase
|
||||
yield options[:options] if block_given?
|
||||
end
|
||||
|
||||
def assert_singleton_routes_for(singleton_name, options = {})
|
||||
(options[:options] ||= {})[:controller] ||= singleton_name.to_s
|
||||
|
||||
full_path = "/#{options[:path_prefix]}#{singleton_name}"
|
||||
with_options options[:options] do |controller|
|
||||
controller.assert_routing full_path, :action => 'show'
|
||||
controller.assert_routing "#{full_path}.xml", :action => 'show', :format => 'xml'
|
||||
end
|
||||
|
||||
assert_recognizes(options[:options].merge(:action => 'update'), :path => full_path, :method => :put)
|
||||
assert_recognizes(options[:options].merge(:action => 'destroy'), :path => full_path, :method => :delete)
|
||||
|
||||
yield options[:options] if block_given?
|
||||
end
|
||||
|
||||
def assert_singleton_named_routes_for(singleton_name, options = {})
|
||||
(options[:options] ||= {})[:controller] ||= singleton_name.to_s
|
||||
@controller = "#{options[:options][:controller].camelize}Controller".constantize.new
|
||||
@request = ActionController::TestRequest.new
|
||||
@response = ActionController::TestResponse.new
|
||||
get :show, options[:options]
|
||||
options[:options].delete :action
|
||||
|
||||
full_path = "/#{options[:path_prefix]}#{singleton_name}"
|
||||
|
||||
assert_named_route "#{full_path}", "#{singleton_name}_path", options[:options]
|
||||
assert_named_route "#{full_path}.xml", "formatted_#{singleton_name}_path", options[:options].merge(:format => 'xml')
|
||||
end
|
||||
|
||||
def assert_named_route(expected, route, options)
|
||||
actual = @controller.send(route, options) rescue $!.class.name
|
||||
assert_equal expected, actual, "Error on route: #{route}(#{options.inspect})"
|
||||
|
||||
Reference in New Issue
Block a user