With resources_controller (svn.ardes.com/rails_plugins/resources_controller) you can quickly add an ActiveResource compliant controller for your your RESTful models.
Here are some examples - for more on how to use RC go to the Usage section at the bottom, for syntax head to resources_controller_for
Here‘s a simple example of how it works with a Forums has many Posts model:
class ForumsController < ApplicationController
resources_controller_for :forums
end
Your controller will get the standard CRUD actions, @forum will be set in member actions, @forums in index.
class PostsController < ApplicationController
resources_controller_for :posts, :in => :forum
end
As above, but the controller will load @forum on every action, and use @forum to find and create @posts
All of the above examples will work for any routes that match what it specified
PATH RESOURCES CONTROLLER WILL DO:
Example 1 /forums @forums = Forum.find(:all)
/users/2/forums @user = User.find(2)
@forums = @user.forums.find(:all)
Example 2 /posts This won't work as the controller specified
that :posts are :in => :forum
/forums/2/posts @forum = Forum.find(2)
@posts = @forum.posts.find(:all)
/sites/4/forums/3/posts @site = Site.find(4)
@forum = @site.forums.find(3)
@posts = @forum.posts.find(:all)
/users/2/posts/1 This won't work as the controller specified
that :posts are :in => :forum
It is up to you which routes to open to the controller (in config/routes.rb). When you do, RC will use the route segments to drill down to the specified resource. This means that if User 3 does not have Post 5, then /users/3/posts/5 will raise a RecordNotFound Error. You dont’ have to write any extra code to do this oft repeated controller pattern.
With RC, your route specification flows through to the controller - no need to repeat yourself.
If you don‘t want to have RC match wildcard resources just pass :load_enclosing => false
resources_controller_for :posts, :in => :forum, :load_enclosing => false
Here‘s an example of a singleton, the account pattern that is so common.
class AccountController < ApplicationController
resources_controller_for :account, :class => User, :singleton => true do
@current_user
end
end
Your controller will use the block to find the resource. The @account will be assigned to @current_user
First thing to do is remove :in => :forum
class PostsController < ApplicationController
resources_controller_for :posts
end
This will now work for /users/2/posts.
How about /account/posts? The account is found in a non standard way - RC won‘t be able to figure out how tofind it if it appears in the route. So we give it some help.
(in PostsController)
map_enclosing_resource :account, :singleton => true, :class => User, :find => :current_user
Now, if :account apears in any part of a route (for PostsController) it will be mapped to (in this case) the current_user method of teh PostsController.
To make the :account mapping available to all, just chuck it in ApplicationController
This will work for any resource which can‘t be inferred from its route segment name
map_enclosing_resource :users, :segment => :peeps, :key => 'peep_id' map_enclosing_resource :posts, :class => OddlyNamedPostClass
Here‘s another singleton example - one where it corresponds to a has_one or belongs_to association
class ImageController < ApplicationController
resources_controller_for :image, :singleton => true
end
When invoked with /users/3/image RC will find @user, and use @user.image to find the resource, and @user.build_image, to create a new resource.
You may have a named route that maps a url to a particular controller and action, this causes resources_controller problems as it relies on the route to load the resources. You can get around this by specifying :resource_path as a param in routes.rb
map.root :controller => :forums, :action => :index, :resource_path => '/forums'
When the controller is invoked via the ’’ url, rc will use :resource_path to recognize the route.
An exmaple app
config/routes.rb:
map.resource :account do |account| account.resource :image account.resources :posts end map.resources :users do |user| user.resource :image user.resources :posts end map.resources :forums do |forum| forum.resources :posts forum.resource :image end map.root :controller => :forums, :action => :index, :resource_path => '/forums'
app/controllers:
class ApplicationController < ActionController::Base map_enclosing_resource :account, :singleton => true, :find => :current_user def current_user # get it from session or whatnot end class ForumsController < AplicationController resources_controller_for :forums end class PostsController < AplicationController resources_controller_for :posts end class UsersController < AplicationController resources_controller_for :users end class ImageController < AplicationController resources_controller_for :image, :singleton => true end class AccountController < ApplicationController resources_controller_for :account, :singleton => true, :find => :current_user end
This is how the app will handle the following routes:
PATH CONTROLLER WHICH WILL DO:
/forums forums @forums = Forum.find(:all)
/forums/2/posts posts @forum = Forum.find(2)
@posts = @forum.forums.find(:all)
/forums/2/image image @forum = Forum.find(2)
@image = @forum.image
/image <no route>
/posts <no route>
/users/2/posts/3 posts @user = User.find(2)
@post = @user.posts.find(3)
/users/2/image POST image @user = User.find(2)
@image = @user.build_image(params[:image])
/account account @account = self.current_user
/account/image image @account = self.current_user
@image = @account.image
/account/posts/3 PUT posts @account = self.current_user
@post = @account.posts.find(3)
@post.update_attributes(params[:post])
Ok - so how do I write the views?
For most cases, just in exactly the way you would expect to. RC sets the instance variables to what they should be.
But, in some cases, you are going to have different variables set - for example
/users/1/posts => @user, @posts /forums/2/posts => @forum, @posts
Here are some options (all are appropriate for different circumstances):
Using the last technique, you might write your posts index as follows (here assuming that both Forum and User have .name)
<h1>Posts for <%= link_to enclosing_resource_path, "#{enclosing_resource_name.humanize}: #{enclosing_resource.name}" %></h1>
<%= render :partial => 'post', :collection => @posts %>
Notice enclosing_resource_name - this will be something like ‘user’, or ‘post’. Also enclosing_resource_path - in RC you get all of the named route helpers relativised to the current resource and enclosing_resource. See NamedRouteHelper for more details.
This can useful when writing the _post partial:
<p>
<%= post.name %>
<%= link_to 'edit', edit_resource_path(tag) %>
<%= link_to 'destroy', resource_path(tag), :method => :delete %>
</p>
when viewed at /users/1/posts it will show
<p> Cool post <a href="/users/1/posts/1/edit">edit</a> <a href="js nightmare with /users/1/posts/1">delete</a> </p> ...
when viewd at /forums/1/posts it will show
<p> Other post <a href="/forums/1/posts/3/edit">edit</a> <a href="js nightmare with /forums/1/posts/3">delete</a> </p> ...
This is like polymorphic urls, except that RC will just use whatever enclosing resources are loaded to generate the urls/paths.
To use RC, there are just three class methods on controller to learn.
resources_controller_for <name>, <options>, <&block>
ClassMethods#nested_in <name>, <options>, <&block>
map_enclosing_resource <name>, <options>, <&block>
If you want to implement something like query params you can override find_resources. If you want to change the way your new resources are created you can override new_resource.
class PostsController < ApplicationController
resources_controller_for :posts
def find_resources
resource_service.find :all, :order => params[:sort_by]
end
def new_resource
returning resource_service.new(params[resource_name]) do |post|
post.ip_address = request.remote_ip
end
end
end
In the same way, you can override find_resource.
You can make use of RC internals to simplify your actions.
Here‘s an example where you want to re-order an acts_as_list model. You define a class method on the model (say order_by_ids which takes and array of ids). You can then make use of resource_service (which makes use of awesome rails magic) to send correctly scoped messages to your models.
Here‘s how to write an order action
def order
resource_service.order_by_ids["things_order"]
end
the route
map.resources :things, :collection => {:order => :put}
and the view can conatin a scriptaculous drag and drop with param name ‘things_order‘
When this controller is invoked of /things the :order_by_ids message will be sent to the Thing class, when it‘s invoked by /foos/1/things, then :order_by_ids message will be send to Foo.find(1).things association
Lets say you want to set to_param to login, and use find_by_login for your users in your URLs, with routes as follows:
map.reosurces :users do |user|
user.resources :addresses
end
First, the users controller needs to find reosurces using find_by_login
class UsersController < ApplicationController
resources_controller_for :users
protected
def find_resource(id = params[:id])
resource_service.find_by_login(id)
end
end
This controller will find users (for editing, showing, and destroying) as directed. (this controller will work for any route where user is the last resource, including the /users/dave route)
Now you need to specify that the user as enclosing resource needs to be found with find_by_login. For the addresses case above, you would do this:
class AddressesController < ApplicationController
resources_controller_for :addresses
nested_in :user do
User.find_by_login(params[:user_id])
end
end
If you wanted to open up more nested resources under user, you could repeat this specification in all such controllers, alternatively, you could map the resource in the ApplicationController, which would be usable by any controller
If you know that user is never nested (i.e. /users/dave/addresses), then do this:
class ApplicationController < ActionController::Base
map_enclosing_resource :user do
User.find(params[:user_id])
end
end
or, if user is sometimes nested (i.e. /forums/1/users/dave/addresses), do this:
map_enclosing_resource :user do
((enclosing_resource && enclosing_resource.users) || User).find(params[:user_id])
end
Your Addresses controller will now be the very simple one, and the resource map will load user as specified when it is hit by a route /users/dave/addresses.
class AddressesController < ApplicationController
resources_controller_for :addresses
end
# File lib/ardes/resources_controller.rb, line 375
375: def self.extended(base)
376: base.class_eval do
377: class_inheritable_reader :resource_specification_map
378: write_inheritable_attribute(:resource_specification_map, {})
379: end
380: end
Include the specified module, optionally specifying which public methods to include
eg
include_actions ActionMixin, :only => :index include_actions ActionMixin, :except => [:create, :new]
# File lib/ardes/resources_controller.rb, line 493
493: def include_actions(mixin, options = {})
494: options.assert_valid_keys(:only, :except)
495: raise ArgumentError, "you can only specify either :except or :only, not both" if options[:only] && options[:except]
496:
497: mixin = mixin.dup
498: if only = options[:only]
499: only = Array(options[:only]).collect(&:to_s)
500: mixin.instance_methods.each {|m| mixin.send(:undef_method, m) unless only.include?(m)}
501: elsif except = options[:except]
502: except = Array(options[:except]).collect(&:to_s)
503: mixin.instance_methods.each {|m| mixin.send(:undef_method, m) if except.include?(m)}
504: end
505: include mixin
506: end
Creates a resource specification mapping. Use this to specify how to find an enclosing resource that does not obey usual rails conventions. Most commonly this would be a singleton resource.
See Specification#new for details of how to call this
# File lib/ardes/resources_controller.rb, line 477
477: def map_enclosing_resource(name, options = {}, &block)
478: spec = Specification.new(name, options, &block)
479: resource_specification_map[spec.segment] = spec
480: end
this will be deprecated soon as it‘s badly named - use map_enclosing_resource
# File lib/ardes/resources_controller.rb, line 483
483: def map_resource(*args, &block)
484: map_enclosing_resource(*args, &block)
485: end
Specifies that this controller is a REST style controller for the named resource
Enclosing resources are loaded automatically by default, you can turn this off with :load_enclosing (see options below)
resources_controller_for <name>, <options>, <&block>
(otherwise these are all inferred from the name)
The default behavior is to set up before filters that load the enclosing resource, and to use associations on that model to find and create the resources. See ClassMethods#nested_in for more details on this, and customising the default behaviour.
By default, a before_filter is added by resources_controller called :load_enclosing_resources - which does all the work of loading the enclosing resources. You can use ActionControllers standard filter mechanisms to control when this filter is invoked. For example - you can choose not to load resources on an action
resources_controller_for :foos skip_before_filter :load_enclosing_resources, :only => :static_page
Or, you can change the order of when the filter is invoked by adding the filter call yourself (rc will only add the filter if it doesn‘t exist)
before_filter :do_something prepend_before_filter :load_enclosing_resources resources_controller_for :foos before_filter :do_something_else # chain => [:load_enclosing_resources, :do_something, :do_something_else]
If you have your own actions module you prefer to use other than the standard resources_controller ones you can set Ardes::ResourcesController.actions to that module to have this be included by default
Ardes::ResourcesController.actions = MyAwesomeActions
Ardes::ResourcesController.singleton_actions = MyAweseomeSingletonActions
class AwesomenessController < ApplicationController
resources_controller_for :awesomenesses # includes MyAwesomeActions by default
end
# File lib/ardes/resources_controller.rb, line 442
442: def resources_controller_for(name, options = {}, &block)
443: options.assert_valid_keys(:class, :source, :singleton, :actions, :in, :find, :load_enclosing, :route, :segment, :as, :only, :except)
444: when_options = {:only => options.delete(:only), :except => options.delete(:except)}
445:
446: unless included_modules.include? ResourcesController::InstanceMethods
447: class_inheritable_reader :specifications, :route_name
448: hide_action :specifications, :route_name
449: extend ResourcesController::ClassMethods
450: helper ResourcesController::Helper
451: include ResourcesController::InstanceMethods, ResourcesController::NamedRouteHelper
452: end
453:
454: before_filter(:load_enclosing_resources, when_options) unless load_enclosing_resources_filter_exists?
455:
456: write_inheritable_attribute(:specifications, [])
457: specifications << '*' unless options.delete(:load_enclosing) == false
458:
459: unless (actions = options.delete(:actions)) == false
460: actions ||= options[:singleton] ? Ardes::ResourcesController.singleton_actions : Ardes::ResourcesController.actions
461: include_actions actions, when_options
462: end
463:
464: route = (options.delete(:route) || name).to_s
465: name = options[:singleton] ? name.to_s : name.to_s.singularize
466: write_inheritable_attribute :route_name, options[:singleton] ? route : route.singularize
467:
468: nested_in(*options.delete(:in)) if options[:in]
469:
470: write_inheritable_attribute(:resource_specification, Specification.new(name, options, &block))
471: end