SuperResources
SuperResources DRYs up your controller code by abstracting your controller's standard RESTful actions and by providing standard helpers to access the controller's target resource(s) in a consistent way across all controllers. More than that, SuperResources exploits the application's routes to provide simplified path helpers for nested resources.
With SuperResources, in the great majority of common REST situations, you can use the same resource helpers and path helpers, regardless of the the specific type of resource or how it is nested, even if the resource is nested under many other resources.
Installation
Add this line to your application's Gemfile:
gem 'super_resources'
And then execute:
bundle install
Or install it yourself as:
gem install super_resources
Usage
To gain all the standard RESTful actions, just include SuperResources::Controller
in your controller:
class OrganizationUnitsController < ApplicationController
include SuperResources::Controller
end
Resource Helper Methods
SuperResources provides helper methods that you can use directly in the controller, views or helper methods:
resource
collection
For member actions, resource
answers a single object that the RESTFUL is operating on.
For collection actions (i.e index
), collection
answer a scoped collection of objects.
SuperResources does away with specifically named instance variables, such as those created by standard scaffolds. For example, in the example above, SuperResources does not give you @organization_unit
or @organization_units
for free. You won't need them in the common cases. Using resource
and collection
in every controller makes for easier coding and maintenance.
Path Helper Methods
SuperResources provides a set of vastly simplified path helpers.
collection_path #=> path to the resource's index action
resource_path(object = resource)
new_resource_path
edit_resource_path(object = resource)
Note that the helper methods that require an input argument, assumes the current resource by default, so you don't have to pass it in.
These Helper Methods Work Even When the Resource is Nested
Because SuperResources uses the metadata created by your routes declarations to work out whether the resource has been nested and if so, automatically uses the nesting objects to build complete paths. This relieves you of passing in an array to the path helper.
With this feature alone, SuperResources cleans up your code and makes it more reusable. For example, the same code can be used when you have a resource that is nested inside multiple other objects. SuperResources dynamically works out the nesting that applies in each case.
Nested Resource
Let's face it: dealing with nested resources has always been a pain. All that fiddling about, getting the path names and inputs right. Change the nesting and you have to go through and change all your path calls too. Then, if you have a resource that can be nested within multiple other resources, such as when you have an associative object and you want to navigate to it from any of it associations, the permutations become very complex.
It should be much easier, especially when you take routes into account. If I have an action that is matched to this route:
/notes/:note_id/pages/:id
then shouldn't I have paths available that can work out the nesting context, so that I don't hard code it?
SuperResources does this. For example, including SuperResources in PagesController
will allow me to simply call,
for example, edit_resource_path
and the code has been actioned through the above route, the nesting with a Note identified by :note_id
will be assumed. Even better, if the same code is actioned by another route with different nesting, SuperResources will get that right too.
If you need, such as when you want to link 'outside the nest' as it were, you still have note_page_path(note, page)
available to you.
Parent Helper Method
Sometimes you want to use the object that is nesting your resource, such as when you want to customize a redirect. SuperResources provides
parent
to answer that object. For example given, the following route:
/notes/:note_id/pages/:id
calling parent
will answer a Note object with an id of :note_id
.
For deeper nests, the immediately nested object is always answered by parent
. For example, given this route:
/authors/:author_id/notes/:note_id/pages/:id
calling parent
will still answer a Note object with an id of :note_id
.
Accessing Route Objects
Any nested route implies a component hierarchy of objects. SuperResources allows you to access these objects bye a convenient name.
For example, given this route:
/authors/:author_id/notes/:note_id/pages/:id
you can make these calls:
author #=> Author with an id of :author_id
note #=> Note with an id of :note_id
page #=> Page with an id of :id
An example of a place where you will want these, is in a layout template that presents information about the nesting objects. For example, you may want to show a page inside an author layout template. The author layout template will present information about the author. In this case, the layout will be used in the context of PagesController
, so you can't refer to resource
in the layout, since it will answer the page, not the author. You need some way to arbitrarily refer to the author. Being able to call author
provides this.
Adapting and Customising
Resource Class
SuperResources derives the class of the target resource from the controller name. For example, OrganizationUnitsController
operates, by default, on resources of the class OrganizationUnit
. If you want to change this, redefine hotspot method resource_class
, for example:
class TeamsController < ApplicationController
include SuperResources::Controller
protected
def resource_class
OrganizationUnit
end
end
Resource Helper Methods
Yes, you can adapt these to suit your needs.
The resource
and collection
methods are used internally by the default RESTful actions provided by SuperResources, and can have their implementations customised easily. The return values of these methods are memoized for various reasons, for this you should make use of the memoize_resource
and memoize_collection
methods respectivley. Both methods take a block that is only executed as necessary.
For example, if you wanted 'TeamsController#collection' to return only those teams that the current user has joined, you would write an implementation of collection
that calls its super, passing a block the evalautes to the right collection:
def collection
memoize_collection { resource_class.joined_by(current_user) }
end
If that looks strange, bear in mind it's been designed so that you don't need to know how SuperResources internally uses your preferred implementation.
Finder Method
Before being able to use the result of resource
, SuperResources may need to find it. The canonical way to do this is to do:
resource_class.find(:params[:id])
While SuperResource uses the find
method as the default, you can choose another finder method by redefining finder_method
. For example:
class PagesController < ApplicationController
protected
def finder_method
:find_by_position
end
end
Builder Method
SuperResources extracts the construction of a new resource into the build_resource
method. If you need to do specialized work for the build, override the build_resource
method and be sure to memoize your result:
def build_resource
memoize_resource do
resource_class.new do |p|
# initialize the state here
end
end
end
Redefining Actions
Yes, you can. All actions defined by SuperResources use responders and accept parameters to pass to respond_with
, so customizing these parameters is a common adaptation.
For example, suppose that after creating a comment, you want to redirect to an index of comments that apply to the same parent. Adapt the action like this:
def create
super :location => polymorphic_url([ parent, :comments ])
end
Anything you can pass to respond_with
, you can pass to the super call, including a block.
You could, of course, completely redefine an action:
def new
# knock yourself out
end
Defining Actions
Just do it. Declare them in your controller and match them in routes. All the SuperResources helpers are still available to you.
Acknowledgments
SuperResources would never have happened without InheritedResources existing first. We preferred the idea of abstracting and extracting RESTful actions out of all our controllers and we're not so keen on scaffolds generating un-DRY code. We used InheritedResources in a production deployed application MeetLinkShare, gained some experience and decided we wanted an even DRYer tool.
The basic mechanics of SuperResources was hacked out during Rails Camp 12 in Tasmania, Australia and was subsequently applied to MeetLinkShare.
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request