Brian Takita's blog
While it is easy to include plugins in your Rails projects, it isn't easy to extend and customize the plugin for your own application. Desert solves that limitation/complication by making it just as easy to extend or modify a plugin class as it would be with any other class. In this post we will go over how Desert provides an easy way to manage and extend your plugins.
At Pivotal, we offer an integrated platform of Rails plugins named Socialitis.
Socialitis is an internal project that grew out of the observation that many of our start-up clients needed to build the same non-differentiating features; user management, friends/contacts, activity feeds, on-site messaging, etc.
The Socialitis platform is broken up into number of plugins that extend the Rails app in specific ways. These plugins may have dependencies on other plugins.
One of the major design goals of Socialitis is easy, drop-in, integration into existing Rails apps. This means using convention over configuration and removing as much integration responsibility from the user of the plugin as possible.
Another design goal was to provide sensible defaults and make each plugin easy to customize for your app.
We used Desert to achieve these goals, and so can you for your own platform.
The major features that Desert provides are:
- Defining a Rail's like directory structure into your plugin (models, views, controllers, helpers)
- Plugin dependencies
- Seamless overriding of classes and modules defined by parent plugins
- Plugin migrations
- Plugin routing
Desert provides a similar feature set to the Radient plugin system and the now defunct Appable Plugins framework.
For a simple example, lets say you have two plugins, name User and Messaging. The User plugin provides basic authentication and login features, and the Messaging plugin allows Users to send Messages to each other. The Message plugin depends on the User plugin.
The directory structure of the full Rails app looks like:
|-- 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
| | |-- logins_controller.rb
| | `-- users_controller.rb
| |-- helpers
| | |-- logins_helper.rb
| | `-- users_helper.rb
| |-- models
| | |-- login.rb
| | `-- user.rb
| `-- views
| |-- logins
| | |-- edit.html.erb
| | |-- index.html.erb
| | |-- new.html.erb
| | `-- show.html.erb
| `-- users
| |-- edit.html.erb
| |-- index.html.erb
| |-- new.html.erb
| `-- show.html.erb
|-- config
| `-- 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
`-- message
|-- app
| |-- controllers
| | `-- message_controller.rb
| |-- helpers
| | |-- message_helper.rb
| | `-- user_helper.rb
| |-- models
| | |-- message.rb
| | `-- user.rb
| `-- views
| `-- messages
| |-- edit.html.erb
| |-- index.html.erb
| |-- new.html.erb
| `-- show.html.erb
|-- config
| `-- routes.rb
|-- db
| `-- migrate
| `-- 001_create_messages.rb
|-- init.rb
|-- spec
| |-- controllers
| | |-- message_controller_spec.rb
| | `-- user_controller_spec.rb
| |-- fixtures
| | |-- messages.yml
| | `-- users.yml
| |-- models
| | |-- message_spec.rb
| | `-- user_spec.rb
| |-- spec_helper.rb
| `-- views
| `-- messages
`-- tasks
The User plugin introduces the various User and Login Rails objects. The Message plugin introduces its respective Message objects. Notice that the Message plugin also reopens some of the User objects to insert functionality.
For example, vendor/plugins/users/app/models/user.rb looks something like:
class User < ActiveRecord::Base
has_many :logins
end
The Message plugin would then reopen User in vendor/plugins/message/app/models/user.rb:
class User < ActiveRecord::Base
has_many :messages_received
has_many :messages_sent
end
Meanwhile, the main application can also reopen User in app/models/user.rb
class User < ActiveRecord::Base
def custom_app_method
# custom app logic #
end
end
Desert allows you to utilize Ruby's ability to repoen classes to layer on functionality in your plugins and application. At Pivotal, we have had success in sharing code across multiple client applications using this technique.
Another thing to note is normally the Message plugin would be loaded before the User plugin. Desert allows you to create plugin dependencies. So in vendor/plugins/message/init.rb:
require_plugin 'user'
require_plugin 'will_paginate'
This means you no longer need to define plugin load order inside of environment.rb. Your plugins can take care of that. Desert works with practically all plugins. That means you can have a plugin dependency on any existing Rails plugin.
To see more examples & documentation, take a look at the Desert project at http://github.com/pivotal/desert.
I just released Cacheable Flash 0.1.4. This version includes test helpers so you can easily test your cache messages. It works by allowing you to make assertions on the flash cookie.
Here is a test/unit example:
require "cacheable_flash/test_helpers"
class TestController < ActionController::Base
def index
flash["notice"] = "In index"
end
end
class ControllerTest < Test::Unit::TestCase
include CacheableFlash::TestHelpers
def setup
@controller = TestController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_cacheable_flash_action
get :index
asset_equal "In index", flash_cookie["notice"]
end
end
Here is a rspec example:
require "cacheable_flash/test_helpers"
class TestController < ActionController::Base
def index
flash["notice"] = "In index"
end
end
describe TestController, "#index" do
include CacheableFlash::TestHelpers
it "writes to the flash cookie" do
get :index
flash_cookie["notice"].should == "In index"
end
end
You can install Cacheable Flash by running:
ruby script/plugin install svn://rubyforge.org/var/svn/pivotalrb/cacheable_flash/trunk
See the Cacheable Flash blog post, Show Flash Messages on Cached Pages, and the README for more information.
Page caching is an easy way to get massive performance and scalability increases with little up front effort. Of course, when page caching, a number of design changes are necessary. For example, server side session data cannot be used to render data on cached pages.
Rails provides the flash hash to easily render alert messages and errors. In the controller you can write to the flash hash:
flash[:error] = "You cannot go there"
# or
flash[:notice] = "Welcome to eternity"
and render the flash hash in the view. Typically the rendering happens on a layout:
<div id="error_div_id" class="flash flash_error"><%= flash[:error] %></div>
<div id="notice_div_id" class="flash flash_notice"><%= flash[:notice] %></div>
There are some strange quirks with using flash such as needing to use FlashHash#now when rendering the response without redirecting.
Everything works great until you need to page cache the landing page.
For example, lets say you page cache your home page. After logging in, you are redirected to the home page with the flash notice "Logged in successfully". When page caching, this solution does not work because the request is responded to by the Web Server (i.e. Apache) and does not reach the Rails App server (i.e. Mongrel).
This means the view does not get a chance to render the flash error and notice.
There are a couple of solutions to this problem.
- Do an AJAX request back to the server to render the flash error and notice (i.e. using RJS)
- Send the flash error and notice from the server with cookies and render it during page load
Introducing Cacheable Flash
To solve the problem using cookies on Peer to Patent, we wrote the Cacheable Flash plugin. The plugin allows you to set the flash hash as normal on the controller. It handles converting the flash hash into cookies. All you need to do is include the CacheableFlash module into your controller.
class ApplicationController < ActionController::Base
include CacheableFlash
# ...
end
On the view side, you will need to add some javascript.
<div id="error_div_id" class="flash flash_error"></div>
<div id="notice_div_id" class="flash flash_notice"></div>
<script type="text/javascript">
Flash.transferFromCookies();
Flash.writeDataTo('error', $('error_div_id'));
Flash.writeDataTo('notice', $('notice_div_id'));
</script>
The Flash.transferFromCookies method:
- Grabs the flash data from the cookies and saves it to Flash.data
- Deserializes the flash hash from JSON to Javascript
- Erases the flash data from the cookies
The Flash.writeDataTo method:
- Writes data from the passed in key in Flash.data to the passed in element or element id
Here is how the life cycle of the login works:
- The user enters the correct login information
Rails handles the web request. In the Login controller, flash[:notice] is written to
if current_user flash[:notice] = "Welcome to Eternity" endAn after filter serializes contents of the Flash Hash as JSON into cookies
- The after filter clears the flash hash
- The cached page is rendered
- The client side receives and clears the flash cookie data
- The client side javascript renders the flash messages
There is also a side benefit -- you don't have to use FlashHash#now because storing the flash in cookies to be rendered and erased by the client makes this unnecessary.
The Cacheable Flash plugin is on the Pivotal RB project page (http://rubyforge.org/projects/pivotalrb). You can install it by running:
ruby script/plugin install svn://rubyforge.org/var/svn/pivotalrb/cacheable_flash/trunk
It will copy flash.js, cookie.js, and json.js if you do not already have these files.
Happy Page Caching!!
Thanks to Josh Susser for pairing with me on this.
We all like a good oxymoron, like redefining constants. There are times where we need to redefine a constant to test an edge case in the application code. Before I go into this example, please note that redefining constants is generally not a good way to have maintainable software. If you find yourself needing to redefine a constant, it may be an indication that refactoring is needed.
Given that, lets get into an example where you may need to redefine a constant. Lets say an app has does file uploads to Amazon's S3 service. A common practice to upload to a real S3 account made for the production, development, or demo environment.
When in the test environment, a fake S3 service would be used instead. The fake service is useful to keep your tests fast and running predictably.
To get a different File Upload service object in each of your environments, one can have the S3 configuration in the environment files:
test.rb
STORAGE_SERVICE = FakeStorageService.new
development.rb
STORAGE_SERVICE = S3StorageService.new("development_service", "access_key", "secret_access_key")
production.rb
STORAGE_SERVICE = S3StorageService.new("production_service", "access_key", "secret_access_key")
The File Upload service objects can be set to constants in the environment file. This works great when testing the logic of the objects that use the File Upload service. However it is a good idea to run an integration test that does a real upload.
Since the tests are running in the test environment, a fake File Upload service is being used. Well now we want to use a real service that points to a test S3 account. An easy trick is to redefine the constant to the S3 service in setup and then redefine the constant back to the fake service on teardown.
There are a few ways of doing this...
Just Reset the Constant
context "A real S3 call" do
setup do
STORAGE_SERVICE = S3StorageService.new("test_service", "access_key", "secret_access_key")
end
teardown do
STORAGE_SERVICE = FakeStorageService.new
end
end
This is the simplest approach, but it produces an error:
warning: already initialized constant STORAGE_SERVICE###Use silence_warnings
context "A real S3 call" do
setup do
silence_warnings do
STORAGE_SERVICE = S3StorageService.new("test_service", "access_key", "secret_access_key")
end
end
teardown do
silence_warnings do
STORAGE_SERVICE = FakeStorageService.new
end
end
end
This solution removes the warning, but now a certain section of your code will not have warning at all. Also, one could argue that you lose semantic meaning. It also feels like a hack.
Redefine the Constant
class Module
def redefine_const(name, value)
__send__(:remove_const, name) if const_defined?(name)
const_set(name, value)
end
end
context "A real S3 call" do
setup do
Object.redefine_const(
:STORAGE_SERVICE,
S3StorageService.new("test_service", "access_key", "secret_access_key")
)
end
teardown do
Object.redefine_const(
:STORAGE_SERVICE,
STORAGE_SERVICE = FakeStorageService.new
)
end
end
Calling redefining the constant does not generate a warning. Also it does provide semantic value because you are actively declaring that you are redefining the constant. If there are other warnings, you will also see them.
Its all Dirty
Redefining constants is a non-standard tatic, especially for those new to Ruby. Since this is unconventional and is often contrary to assumptions, it may lead to unpredictable behavior.
Maybe the storage service can be an attribute that can be changed for individual tests.
