Desert - It doesn’t get any DRYer than this
Desert is a Rails plugin framework that makes it easy to share models, views, controllers, helpers, routes, and migrations across your applications.
With Desert, reusability doesn’t come at the cost of extensibility: it’s trivial to extend the functionality of a plugin - both in your application and in other plugins.
Classes are automatically mixed in with your own or other plugins’ classes. This allows you to make full featured composable components.
Desert is a replacement for Appable Plugins (wiki.pluginaweek.org/Appable_plugins).
Bug/Feature Tracker
Pivotal Tracker: www.pivotaltracker.com/projects/358
Anatomy of a desert plugin
|-- app
| |-- controllers
| | |-- application.rb
| | `-- blogs_controller.rb
| |-- helpers
| | |-- application_helper.rb
| | `-- blogs_helper.rb
| |-- models
| | `-- user.rb
| `-- views
| |-- blogs
| |-- layouts
| | `-- users.html.erb
| `-- users
| |-- index.html.erb
| `-- show.html.erb
|-- db
| `-- migrate
| `-- 001_migrate_users_to_001.rb
|-- lib
| `-- current_user.rb
|-- spec
| |-- controllers
| | `-- blogs_controller_spec.rb
| |-- fixtures
| |-- models
| |-- spec_helper.rb
| `-- views
| `-- blogs
`-- vendor
`-- plugins
`-- user
|-- app
| |-- controllers
| | `-- users_controller.rb
| |-- helpers
| | `-- users_helper.rb
| |-- models
| | `-- user.rb
| `-- views
| `-- users
| |-- edit.html.erb
| |-- index.html.erb
| |-- new.html.erb
| `-- show.html.erb
|-- config
| `-- desert_routes.rb
|-- db
| `-- migrate
| `-- 001_create_users.rb
|-- init.rb
|-- lib
| `-- current_user.rb
|-- spec
| |-- controllers
| | `-- user_controller_spec.rb
| |-- fixtures
| | `-- users.yml
| |-- models
| | `-- user.rb
| |-- spec_helper.rb
| `-- views
| `-- users
`-- tasks
Installation and Usage
-
Install the gem
sudo gem install desert
-
Require ‘desert’ between ‘boot’ and Rails::Initializer.run in environment.rb
# File: config/environment.rb require File.join(File.dirname(__FILE__), 'boot') require 'desert' Rails::Initializer.run do |config| end
NOTE: you may have to require rubygems before requiring desert.
-
Generate your desert plugin
script/generate desert_plugin my_plugin_app
Manage Plugin Dependencies
By default, Rails loads plugins in alphabetical order, making it tedious to manage dependencies. Desert will automatically load plugins in the proper order when you declare their dependencies like this:
# File: vendor/plugins/blogs/init.rb
require_plugin 'user'
require_plugin 'will_paginate'
Here user
and will_paginate
will always be loaded before <tt>blogs<tt>. Note that any plugin can be declared as a dependency.
Share Routes
When you share controllers, you’ll want to share their routes too. If you look in your RAILS_ROOT/config/routes.rb file you will notice that the generator added a new line to the top:
map.routes_from_plugin(:my_plugin_app)
In the user
plugin:
# File: vendor/plugins/user/config/desert_routes.rb
resource :users
In the blogs
plugin:
# File: vendor/plugins/blogs/config/desert_routes.rb
resource :blogs
In the application:
# File: config/desert_routes.rb
ActionController::Routing::Routes.draw do |map|
map.routes_from_plugin :blogs
map.routes_from_plugin :user
end
Here the application adds the users
resource from the user
plugin and the blogs
resource from the blogs
plugin. Notice that there is no need to call methods on map in the plugin route files, because they are instance eval’d in the map object.
All standard routing methods are available in your plugin’s routes file, such as:
namespace :admin do |admin|
admin.resources :posts
end
Desert uses a separate table to manage migration version to maintain backwards compatibility with Rails 1.x. Your plugin app’s migration live in your_plugin/db/migrate. To run migrations, follow these steps:
-
Create a new migration in your main app
script/generate migration migrate_my_plugin_to_045
-
Add the custom ‘migrate_plugin` method
class MigrateMyPluginTo045 < ActiveRecord::Migration def self.up migrate_plugin(:my_plugin, 20080530223548) end def self.down migrate_plugin(:my_plugin, 0) end end
-
Run your migrations normally
rake db:migrate connect "/signup", :controller => "users", :action => "signup"
Share Migrations
Sharing models means sharing schema fragments, and that means sharing migrations:
In the user
plugin:
vendor/plugins/user/db/migrate/
001_create_user_table.rb
In the blogs
plugin:
vendor/plugins/blogs/db/migrate/
001_create_user_table.rb
002_add_became_a_blogger_at_to_user.rb
Here the blogs
plugin needs to add a column to the users
table. No problem! It just includes a migration in its db/migrate
directory, just like a regular Rails application. When the application developer installs the plugin, he migrates the plugin in his own migration:
application_root/db/migrate/009_install_user_and_blogs_plugins.rb
class InstallUserAndBlogsPlugins < ActiveRecord::Migration
def self.up
migrate_plugin 'user', 1
migrate_plugin :blogs, 2
end
def self.down
migrate_plugin 'user', 0
migrate_plugin :blogs, 0
end
end
Here the application migrates the user
plugin to version 1 and the blogs
plugin to version 2. If a subsequent version of the plugin introduces new migrations, the application developer has full control over when to apply them to his schema.
Share Views
To share views, just create templates and partials in the plugin’s app/views
directory, just as you would with a Rails application.
application_root/app/views/blogs/index.html.erb
<%= @blog.posts.each do |post| %>
...
<% end %>
Customize / extend behavior in each installation
Say you want to create a plugin named acts_as_spiffy. Desert allows Spiffy to have a set of features that can be reused and extended in several projects.
The Spiffy project has a:
-
SpiffyController
-
Spiffy model
-
SpiffyHelper
-
spiffy.html.erb
-
SpiffyLib library class
The Spiffy plugin acts as its own mini Rails application. Here is the directory structure:
RAILS_ROOT/vendor/plugins/spiffy/app/controllers/spiffy_controller.rb
RAILS_ROOT/vendor/plugins/spiffy/app/models/spiffy.rb
RAILS_ROOT/vendor/plugins/spiffy/app/helpers/spiffy_helper.rb
RAILS_ROOT/vendor/plugins/spiffy/app/views/spiffy/spiffy.rhtml
RAILS_ROOT/vendor/plugins/spiffy/lib/spiffy_lib.rb
Now, say there is a Spiffy Store rails application that uses acts_as_spiffy. The Rails app can open up any of the Spiffy classes and override any of the methods.
Say spiffy.rb in the Spiffy plugin is defined as:
class Spiffy < ActiveRecord::Base
def why?
"I just am Spiffy"
end
end
The Spiffy#why method can be overridden in RAILS_ROOT/app/models/spiffy.rb
class Spiffy < ActiveRecord::Base
def why?
"I sell Spiffy stuff"
end
end
Running plugin tests
You can run your plugin tests/specs like so:
rake desert:testspec:plugins PLUGIN=spiffy
Leaving off the PLUGIN environment variable will cause it to run all the test/specs for all installed plugins, which may not be what you want.
Running Desert Specs
To run specs, you need to:
-
Make sure you have the necessary gems installed:
sudo geminstaller
-
On OSX, you may have to manually install sqlite3-ruby gem
sudo env ARCHFLAGS=“-arch i386” gem install sqlite3-ruby
-
If sqlite3-ruby fails to compile, install it.
OSX: sudo port install sqlite3 Debian: sudo aptitude install sqlite sqlite3 libsqlite-dev libsqlite3-dev
-
Install git git.or.cz/
-
Install the dependencies
rake install_dependencies
-
Run the specs
rake
Notes on Rails version dependencies
Desert is a library that heavily monkey patches Rails. To ensure that Desert works with multiple versions of Rails, its tests are run against the supported versions of Rails.
To set up the different supported versions of Rails, run
rake install_dependencies
This will clone the Rails git repo and export the supported versions of rails into the respective directories.
rake update_dependencies
will update the clones repo on your machine.