merb_resource_controller

A merb plugin that provides the default CRUD actions for controllers and allows for easy customization of the generated actions. merb_resource_controller only supports datamapper so far. Implementing support for active_record would be trivial, but I have no need for it.

Currently, merb_resource_controller needs an extra specification inside the controller to define the nesting strategy to use. This shouldn’t really be necessary, since this information could be provided by the Merb::Router. I’m planning to have a look into this soon! With that in place, nested resource_controllers could be 3 liners that just do what you would expect them to do.

To get you started with merb_resource_controller, all you have to do is extend it into your controller. Most probably you will want extend it into the Application controller, which will make the controlling method available to all your application’s controllers.


class Application < Merb::Controller
  extend Merb::ResourceController::Mixin::ClassMethods
end

And here is a quick example showing a controller for a standard toplevel resource with all standard CRUD actions.


class Articles < Application
  controlling :articles
end

If you don’t want all standard CRUD actions to be generated, give merb_resource_controller the exact list of actions you want your controller to have.


class Articles < Application
  controlling :articles do |a|
    a.action :create
    a.action :update
  end
end

Alternatively, you can also add more than one action at once using the actions method available on the ResourceProxy


class Articles < Application
  controlling :articles do |a|
    a.actions :create, :update
  end
end

If you are using models that are nested inside some module (maybe you’re developing a slice ?), you need to provide the fully qualified class name to the controlling method. Per default, merb_resource_controller assumes that you don’t want to use the fully qualified name to generate your instance variables and association calls.


class Ratings < Application
  controlling "Community::Rating"
end

This will initialize the instance variable @ratings (or @rating respectively). If this resource is nested below another resource (see next section), this also means that the :ratings (or :rating respectively) association will be used to navigate from the parent resource down to this resource.

If however, you want to use fully qualified names for your instance variables and association calls, you can explicitly pass :fully_qualified => true to the controlling method (false is the default).


class Ratings < Application
  controlling "Community::Rating", :fully_qualified => true
end

This will initialize the instance variable @community_ratings (or @community_rating respectively). If this resource is nested below another resource (see next section), this also means that the :community_ratings (or :community_rating respectively) association will be used to navigate from the parent resource down to this resource.

Nested Resources

merb_resource_controller will automatically recognize if it is accessed from a toplevel or a nested url, and will adjust its actions accordingly. It will also initialize all the instance variables you would expect for your chosen nesting strategy. This means, that when you have an URL like /articles/1/comments, you will have an @article variable pointing to Article.get(1) and a @comments variable pointing to Article.get(1).comments.

It’s important to note, that no database operations will be performed more than once! So in the case of /articles/1/comments/1/ratings, merb_resource_controller will do exactly what you would expect it to do: It will issue Article.get(1).comments.get(1).ratings, initializing @article, @comment and @ratings with the intermediate results on it’s way down the association chain.

Speaking of an association chain, it is worth mentioning that it is by no means necessary that your underlying models are connected via real datamapper associations (like has n, ... or belongs_to). The only requirement is, that the object returned from a call to the nested collection respond_to?(:get) (in most cases it will be a DataMapper::Collection anyway).


</code>
class Comments < Application
  controlling :comments do |c|
    c.belongs_to :article
  end
end

class Ratings < Application
  controlling :ratings do |r|
    r.belongs_to [ :article, :comment ]
  end
end

Nested Singleton Resources

First of all, singleton resources (currently) must always be nested below at least one parent resource.

This is mostly because I somehow can’t really imagine a usecase for a real toplevel singleton resource? I mean, there must be some information on how to load that one resource. Since we’re mostly dealing with database backends here, we should probably need some kind of key to identify exactly one dataset, but we don’t get that key from a resource like /profile (where is the :id param here?). So my guess is, that most of the toplevel singleton resources that are actually used, really are nested below some kind of resource that’s stored in the session, most probably the authenticated user object. If this is the case, it would be perfectly valid, to just really nest those singletons below their real parent, and probably add a route alias to keep up the disguise. If you can come up with a usecase where you have a real toplevel singleton resource that needs no key to be loaded, just don’t use merb_resource_controller. Really, it’s that simple! If however, I’m completely on the wrong track here, I would be glad to be informed about it, so that I can fix things up.

The example code below behaves almost exactly like described in the section on Nested Resources above. The only two differences are, that 1) no index action will be generated and that 2) the initialized instance variables will be named @article and @editor if you have a singleton resource nested like /articles/1/editor. Just like you would expect, no?


class Editors < Application

  controlling :editor, :singleton => true do |e|
    e.belongs_to :article
  end

end

DataMapper’s Identity Map

Including Merb::ResourceController::DM::IdentityMapSupport into any of your controller will wrap all actions inside a dm repository block in order to be able to use DM’s identitiy map feature. For more information on that topic, have a look at DataMapper’s Identity Map

Feel free to include this module into single controllers instead of including it into Application controller, which enables identity maps for every controller action in your app.


class Application < Merb::Controller  
  include Merb::ResourceController::DM::IdentityMapSupport
end

Action Timeouts

Extending Merb::ResourceController::ActionTimeout into any of your controllers, will give you the set_action_timeout method. If you specify an action_timeout greater or equal to 1 and you have the system_timer gem installed, the action will timeout after the given amount of seconds. For a more detailed explanation on the topic have a look at Battling Wedged Mongrels with a Request Timeout

Feel free to set action timeouts for single controllers instead of doing so in Application controller, which sets the same action timeout for every controller action in your app.


class Application < Merb::Controller
  extend Merb::ResourceController::ActionTimeout  
  set_action_timeout 1
end

Defined Actions

All the above controllers will have the following actions defined. Feel free to override them, to customize how the controllers behave. Of course you are also free to override all the methods used by these defined actions.

  
def index
  set_action_specific_provides(:index)
  load_resource
  display requested_resource
end

def show
  set_action_specific_provides(:show)
  load_resource
  raise Merb::ControllerExceptions::NotFound unless requested_resource
  display requested_resource
end

def new
  set_action_specific_provides(:new)
  load_resource
  set_member(new_member)
  display member
end

def edit
  set_action_specific_provides(:edit)
  load_resource
  raise Merb::ControllerExceptions::NotFound unless requested_resource
  display requested_resource
end

def create
  set_action_specific_provides(:create)
  load_resource
  set_member(new_member)
  if member.save
    handle_successful_create
  else
    handle_failed_create
  end
end

def update
  set_action_specific_provides(:update)
  load_resource
  raise Merb::ControllerExceptions::NotFound unless requested_resource
  if requested_resource.update_attributes(params[member_name])
    handle_successful_update
  else
    handle_failed_update
  end
end

def destroy
  set_action_specific_provides(:destroy)
  load_resource
  raise Merb::ControllerExceptions::NotFound unless requested_resource
  if requested_resource.destroy
    handle_successful_destroy
  else
    handle_failed_destroy
  end
end

Additional Methods to override

In addition to the actions and the methods they are using, you will probably want to override some other methods that merb_resource_controller uses to do its thing. Here are some default method implementations you can override to this effect:

Customize the create action


def handle_successful_create
  handle_content_type(:create, content_type, :success)
end

def handle_failed_create
  handle_content_type(:create, content_type, :failure)
end


def html_response_on_successful_create
  options = flash_messages_for?(:create) ? { :message => successful_create_messages } : {}
  redirect redirect_on_successful_create, options          
end
        
def html_response_on_failed_create
  message.merge!(failed_create_messages) if flash_messages_for?(:create)
  render :new, :status => 406         
end

        
def xml_response_on_successful_create
  display member, :status => 201, :location => resource(member)
end
        
def xml_response_on_failed_create
  display member.errors, :status => 422
end        

     
def json_response_on_successful_create
  display member, :status => 201, :location => resource(member)         
end
        
def json_response_on_failed_create
  display member.errors, :status => 422
end


def redirect_on_successful_create
  target = singleton_controller? ? member_name : member
  resource(*(has_parent? ? parents + [ target ] : [ target ]))
end

# These are available by default (i.e. if all actions are available)
# If you specify specific actions inside the block passed to #controlling
# you need to pass the :flash option to the ResourceProxy#action like so:
# action :create, :flash => true

def successful_create_messages
  { :notice => "#{member.class.name} was successfully created" }
end

def failed_create_messages
  { :error => "Failed to create new #{member.class.name}" }
end

Customize the update action

<pre> def handle_successful_update handle_content_type(:update, content_type, :success) end

def handle_failed_update handle_content_type(:update, content_type, :failure) end

def html_response_on_successful_update options = flash_messages_for?(:update) ? { :message => successful_update_messages } : {} redirect redirect_on_successful_update, options end

def html_response_on_failed_update message.merge!(failed_update_messages) if flash_messages_for?(:update) display requested_resource, :edit, :status => 406 end

def xml_response_on_successful_update "" # render no content, just 200 (OK) status. end

def xml_response_on_failed_update display member.errors, :status => 422 end

def json_response_on_successful_update "" # render no content, just 200 (OK) status. end

def json_response_on_failed_update display member.errors, :status => 422 end

def redirect_on_successful_update target = singleton_controller? ? member_name : member resource(*(has_parent? ? parents + [ target ] : [ target ])) end

  1. These are available by default (i.e. if all actions are available)
  2. If you specify specific actions inside the block passed to #controlling
  3. you need to pass the :flash option to the ResourceProxy#action like so:
  4. action :update, :flash => true

def successful_update_messages { :notice => “#membermember.classmember.class.name was successfully updated” } end

def failed_update_messages { :error => “Failed to update #membermember.classmember.class.name” } end

Customize the destroy action

<pre> def handle_successful_destroy handle_content_type(:destroy, content_type, :success) end

def handle_failed_destroy raise Merb::ControllerExceptions::InternalServerError end

def html_response_on_successful_destroy options = flash_messages_for?(:destroy) ? { :message => successful_destroy_messages } : {} redirect redirect_on_successful_destroy, options end

def xml_response_on_successful_destroy "" # render no content, just 200 (OK) status. end

def json_response_on_successful_destroy "" # render no content, just 200 (OK) status. end

def redirect_on_successful_destroy if singleton_controller? has_parent? ? resource(parent) : ‘/’ else resource(*(has_parent? ? parents + [ collection_name ] : [ collection_name ])) end end

  1. These are available by default (i.e. if all actions are available)
  2. If you specify specific actions inside the block passed to #controlling
  3. you need to pass the :flash option to the ResourceProxy#action like so:
  4. action :destroy, :flash => true

def successful_destroy_messages { :notice => “#{member.class.name} was successfully destroyed” } end

def failed_destroy_messages { :error => “Failed to destroy #{member.class.name}” } end

More Information

Have a look at Merb::ResourceController::Actions, Merb::ResourceController::Mixin::InstanceMethods and Merb::ResourceController::Mixin::FlashSupport inside lib/merb_resource_controller/actions.rb and lib/merb_resource_controller/resource_controller.rb to see what methods will be available inside your controllers. If you want to see where the real work gets done_, have a look at @lib/merb_resource_controller/resourceproxy.rb@

Of course, you should also have a look at the specs, to get a more concrete idea of how merb_resource_controller behaves under the given circumstances.

TODO

  1. Infer route nesting strategy from Merb::Router
  2. Support for user stamps (aka created_by and updated_by)
  3. Support for pagination once an official merb pagination solution exists