Love for Rails 3 Engines
Posted by Elliott Golden
Until recently, I didn't really know what a Rails Engine was. Is an Engine something completely different than a plugin or Gem? Does it stand alone from the Rubygems ecosystem, or is it something that is integrally connected to it? All were questions of mine.
I struggled for a couple weeks setting up a smart development and test env for a CMS Gem I have been working on. Having little Gem/plugin dev experience didn't help either (especially when dealing with a full stack Gem such as a CMS).
I had decided to go with a development structure that I had read of a couple times and that made sense to me. I created a new Rails app. In the app's vendor dir I built out the core structure of the Gem itself within a lib dir. Within lib was all my implementation code, Cucumber features and RSpec directories. To avoid repeatedly building (Jeweller: rake install) the Gem every time I ran a feature or spec, I had a ton of implementation and test dirs packed onto $LOAD_PATH. These stanky $LOAD_PATH additions and their subsequent requires were littered all about my Gem's root init file. It smelled even worse in Cucumber's env.rb and Rspec's spec_helper.rb. But since I was basically wiring the Gem into the host app instead of letting the host app simply bundle my Gem, that's what was needed. My last two posts reveal a bit more about the kludgery that was a foot.
Engines to the rescue, sort of... Learning about Engines ultimately put me on a better path regarding the structuring of the Gem, its test environment and the host app the enables it all. The new dev structure is as follows: On the root, I have my Gem's implementation within lib, sibling to that is spec_env. spec_env is a full-blown "dummy" Rails app that now contains the Cucumber and RSpec dirs. From the Gemfile of the dummy app I include the Gem via the convenient Bundler path argument like so:
#spec_env/Gemfile
gem 'my_gem', :path => '../../'
With these changes, everything just kinda fell into place. I felt like I was developing for Rails again. Among many other things, my features' and specs' rake tasks could now be found by default and the Gem's root init file was far leaner. These structural changes actually have little to do with Engines however. The Rails::Engine class can now come into play because the host app is now able to require the CMS Gem as Rails intended.
An Engine is a class, you write it into your Gem or plugin. It doesn't stand alone from that context. Looking at its source reveals that by default the Rails::Engine class predominantly tasks itself with adding conventionally structured Rails directories to $LOAD_PATH. Among other things, it also provides an interface for hooking initializers into the load process and enables customization of the default Rails dir mappings within the Gem/plugin's context. What this means, is, that if your Gem's init file requires your engine.rb, your host app can now call controllers, models and routes etc.. from your Gem as if they were "native". Now you can forget explicit $LOAD_PATH additions for your Gem's Rails dirs. Rails::Engine handle this for you. For smaller Gem's Engines may not be necessary, but for larger libraries that are structured predominantly like a Rails app, it's a big win in simplicity.
A little code to show how easy an Engine is to implement should about wrap things up.
# lib/engine.rb
require "my_gem"
require "rails"
module MyGem
class Engine < Rails::Engine
# Desired config code here, if any.
end
end
# lib/my_gem.rb
require 'engine' if defined?(Rails)
That's it. If you want to configure the Engine beyond the default, spend a minute with railties/lib/rails/engine.rb to learn about the options. Beyond that, have a look at master. Looks like some nice things are already available for edge Rails. 3.1 should be a treat.
One final tip. If you have an app the will only have several concurrent users (I think a CMS fits into this category). You may find it convenient to serve static files directly from your Gem (The alternative is to copy them over to the host app's public dir). The code below does the trick. Thanks be to Jon Swope for this snippet rails-3-engines-plugins-and-static-assets .
# lib/engine.rb
require "my_gem"
require "rails"
module MyGem
class Engine < Rails::Engine
initializer "static assets" do |app|
app.middleware.use ::ActionDispatch::Static, "#{root}/public"
end
end
end
More:
Gist by José Valim on related topics
Keith Schacht on Rails 3 engine and Gems