At Gigster, we are big fans of DRY. That is why we are costantly developing and maintaining a plethora of libraries and frameworks that can be reused accross different projects. Sometimes, these are just small tools that help us solve common pains, but we also have templates for popular project types.
Recently, I have been asked to develop a reusable template for crowdfunding websites. This template would take the form of a framework that could be plugged into an existing application to provide Kickstarter-esque features.
I knew about Rails engines (and, like most Rails developers, I use lots of them daily) but I had never had the time nor the necessity to create one myself. So, armed with the relevant Rails guide, I set out to create my first mountable Rails engine.
What follows is a list of the technical challenges I have encountered during the development of Crowdster and how I solved them. These are not new patterns: all of them can be found in renowned Rails engines.
The configuration block pattern
The first thing I needed to do was provide my users with a way to configure the engine.
Almost all configurable engines expose an API like the following to be used in a Rails initializer:
There are some variations, but this is the core idea: you expose a method which yields a configuration object. This object is then manipulated by the user to configure the engine.
Starting from the result I wanted to achieve, I created a Configuration class that would simply serve as a container for all configuration options:
Now that I had the class, all I needed was a point of entry:
I also added some logic for validating the provided configuration, but that is beyond the scope of this article and you can easily figure that out yourself.
Base controller inheritance
This one was a bit trickier. Since Crowdster does not take care of authentication, I needed access to some helpers in the parent application’s base controller.
After many frustrating, unsuccessful and over-engineered solutions, I decided to simply allow setting the base controller’s name via the configuration object and then extend it in the engine’s ApplicationController, much like this:
Then, in the initializer:
All engine controllers extend ApplicationController, so I had now access to the helpers from the parent app.
I am still not sure if this is the best approach or if it could cause any conflicts, but it’s been working well for us so far.
Extending classes (via class_eval)
Now that I had the basics in place, I could start the actual development.
I created a model in the engine that belonged to the user class in the parent app. The class name was, once again, configurable via the initializer, so everything was working fine.
However, now I needed to create the corresponding has_many relationship on the user model in the parent app. How could I accomplish this?
The approach I settled on in the end is fairly simple:
I have also explored other solutions, which I think are equally valid, although more suitable for situations where you want multiple classes to assume the same behavior.
Extending classes (via acts_as_*)
One pattern that is very popular among many engines is the acts_as pattern.
What this means is that you patch ActiveRecord::Base (or its equivalent in your ORM) to provide a class method that injects the desired behavior into the current class.
In my case, I would create an acts_as_campaigner method that would then inject the relevant logic into the user model:
With the acts_as approach, it is possible to define per-model configuration, making our engine quite more flexible.
Extending classes (via modules)
Have you noticed how we used a module to extend the functionality of ActiveRecord::Base instead of opening the class directly?
We can use a similar approach to extend the User class:
This has the benefit of saving us lines of code and being more isolated, since we are not polluting ActiveRecord::Base, but it’s not as configurable as the acts_as approach.
Testing the engine with RSpec
By default, the Rails generator uses MiniTest for testing the engine.
If you’re a fan of RSpec like I am, you will have to do some adjustments in order for your specs to run correctly.
After installing RSpec, move the test/dummy directory to spec/dummy, then delete the test directory. Now, edit your rails_helper.rb to look similar to this:
This way, RSpec will load the environment and database migrations from the dummy app, instead of looking for them in the engine.
When you create a database migration, you will need to copy it into the dummy app before executing it:
$ cd spec/dummy
$ rake my-engine:install:migrations
$ rake db:migrate
For controller specs to run, you will also need to tell RSpec which route collection to use:
Et voilà! You can now use your testing framework of choice!