I came across an interesting challenge a few days ago. We have a number of Rails application in our system, architected differently, using different gems, doing different things. However, across the board, they all maintain a single piece of shared functionality – the ability to share their status.
What do we want?
- The ability to share the fact that they’re running
- The ability to share the version of the code that they’re running
No matter the purpose of each individual app, they each share this functionality. And this functionality is completely decoupled from the rest of the app. So why not DRY it up?
I tried a number of approaches to this problem. I tried monkeypatching ActionController, re-drawing Rails routes dynamically, and even copy+pasting the controller. None of these approaches worked well and were an acceptable solution to me. Eventually, I stumbled upon Rails Engines, and realized this was the best solution to this problem.
What are Rails Engines?
Engines are mini-Rails apps (typically bundled as a Ruby gem) that provide functionality to the parent app. An engine’s directory structure mimics the structure of a Rails app. Any controller you add to
app/controllers, any model you add to
app/models, any view you add to
app/views will be available to the parent app. Chances are, you’ve used a Rails engine in your application before. devise is a popular one.
How do I create a Rails engine?
Creating a Rails engine is as simple as including the
rails gem in your
gemspec, creating a new class and extending from
Rails::Engine. However, Rails offers a great tool to automatically create an Engine gem and set up the directory layout.
rails plugin new v8 --mountable
This will create a number of directories and files, but these are the ones you need to know:
app- Just like the
appdirectory in your Rails app. This is where you put your Engine controllers, models, views, helpers, mailers, etc.
config/routes.rb- Just like the
routes.rbfile in Rails. Here, you define your Engine specific routes. Later, in your Rails app’s
routes.rb, you can choose to mount this engine and your app will have all of the routes you define here.
lib- This is the
libfolder like in any gem. Here, you can configure your Engine, and add any non-Rails code that you need.
Take a look at
lib/v8/engine.rb. You’ll see the line
isolate_namespace V8. This line is extremely important because it namespaces everything in your gem under your gem name. Without this line, every single model and controller will be namespaced under the base namespace, so you may experience collisions if your Rails app defines models or controllers with the same name.
How do I share functionality?
The purpose of this post is to teach you how to share functionality between Rails apps using Engines, so lets get to the point!
We want to define a simple controller,
PingController, that accepts a GET request to /ping and returns “pong”.
module V8 class PingController ''` Now, run `rake routes` again...
$ rake routes
Prefix Verb URI Pattern Controller#Action
v8 / V8::Engine
Routes for V8::Engine:
ping GET /ping(.:format) v8/ping#ping
Much better! Now we have a fully functional PingController and its route added to our Rails application, by installing 1 gem and adding 1 line to `routes.rb`. Engines are an extremely powerful way to separate functionality and reuse code across applications.