h2. 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.
h2. 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).
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
h2. 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
h2. 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":http://datamapper.org/doku.php?id=docs: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
h2. 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":http://adam.blog.heroku.com/past/2008/6/17/battling_wedged_mongrels_with_a/
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
h2. 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
h2. 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:
h3. 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
h3. Customize the update action
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
# 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 :update, :flash => true
def successful_update_messages
{ :notice => "#{member.class.name} was successfully updated" }
end
def failed_update_messages
{ :error => "Failed to update #{member.class.name}" }
end
h3. Customize the destroy action
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
# 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 :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
h2. 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/resource_proxy.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.
h2. TODO
# Infer route nesting strategy from @Merb::Router@
# Support for user stamps (aka created_by and updated_by)
# Support for pagination once an _official_ merb pagination solution exists