Rails Generators help streamline spinning up new code. Whether it’s a new model, controller, resource, migration, job, or something else, generators remove a lot of the tedious leg work of bootstrapping new functionality. While anybody that’s used Rails has likely run one of the built-in generators at once or twice, there’s some real power in creating custom generators.
I’ve been spending more time creating custom generators, but the more I have, the more I’ve collected notes on the rough edges to make it a little easier every time. So if you had no idea you could create your own generators, we’ll explore how it works by getting into some of the lower-level details for a basic generator.
On the other hand, if you already knew and you’ve even created some custom generators yourself but stumbled enough that you don’t turn to them unless you really need to, hopefully some of these notes and references can help.
A Little Preparation
We’re not going to go super-deep in customizing the generators to support different test suites, ORMs, or any of that. We’ll keep things simple and focus on building a smaller generator to dial in the basics. Then you can create generators to help with small repetitive tasks in your project or use this as a stepping stone towards more advanced generators. Before we get started, here’s a few links to keep handy for additional reference:
- The Rails Generators Guide
- Rails::Generators Docs
- Rails::Generators::TestCase Docs
- The Thor Wiki - Thor is the underlying toolkit that powers Rails’ generators.
- The Source for the Built-in Rails Generators - These can be fairly complex because they’re some of the more advanced examples you can find, but some like the task generator provide good self-contained examples to learn from.
- The Rails Tests for its own Generators - Like the generators themselves, the tests can be a bit overwhelming at first for the more complex generators, and some of the testing functionality has been extracted into shared modules. Again, some like the task generator tests are a good starting point that’s less overwhelming.
It’s definitely worth it to spend at least a little time perusing some of those links before diving in. I’ll do my best to provide everything you need right here, but coming into the process with more context would likely help before going much further.
I’m writing a book to help streamline creating custom Rails generators so you can save time and skip all that copying and pasting and searching and replacing. Join the list to be notified about the release in September 2023.
In the interest of keeping things simple, we’ll create a generator that makes it easier to spin up a PORO and a corresponding test file to go with it. For now, the generated code isn’t the critical part. We’re going to focus purely on the generation portion of the process and keep the generated code as boring as possible.
Generating a Generator
Not to get too meta, but Rails provides a built-in generator for generating generators. We’ll use that and jump right in, but remember that you can append the
--pretend option if you prefer to see a preview of the files it will generate ahead of time.
You should end up seeing results that look like this:
The generator provides a few things for us inside of the
lib/generators/plain. It adds the generator itself, a file for explaining the usage, a folder for the templates that we’ll use to generate our actual files, and the test file for the generator. So far so good. Let’s look at how the generator starts off for each of these.
The Generator File
The generator itself starts fairly simply, but don’t let that fool you. (Figure 3) There’s some pretty great functionality exposed to your generator through its parent class, so let’s zoom in on the parent class: Rails::Generators::NamedBase. The parent class is interesting both for what it provides directly and what it exposes from the grandparent class, Rails::Generators::Base, which which provides a plethora of convenience methods for the types of file system work you’ll likely want to perform in your generator.
While Rails::Generators::NamedBase is the default parent used in a new generator, it’s not strictly necessary to use it. In some cases, you may want to use Rails::Generator::Base directly instead , but we’ll get to that in detail later.
For the most part,
NamedBase adds convenience methods related to the name passed to the generator, but it also makes a key assumption about arguments in that the first value passed to it gets parsed as the
name upon which many of the additional convenience methods act. So with
NamedBased your generator command restricts you a bit with that first argument. (Figure 4)
In most cases, using
NamedBase is what you want, but in some simple cases, I’ve found it handier to use
Base instead. With the former, the first command line argument will be captured as the
name, but with the latter, there are no pre-defined arguments so you have complete control over that first argument.
For example, I use a generator to start to new blog posts. When I start a new post, the generator really only needs a title in quotes, but with
NamedBase, the command “swallows” the initial portion of the title string up to the first space. So it would require passing an argument before the title in order to fulfill the
name argument’s position. For this context, however, I don’t have a use for the
name argument. So by inheriting from
Base instead, I’m able to skip passing an otherwise unused
name value. (Figure 5)
So while you likely want the default
NamedBase in most cases, don’t forget you can also use
Base for simpler cases. We’ll use
NamedBase for our example here, but I wanted to make sure and draw attention to this detail because it’s not explicitly discussed in the guides or documentation. It wasn’t until reading through the source code that I found out why the arguments I was passing in my tests were’t captured the way I expected them to be. Once you know, you know, but knowing ahead of time would have saved me some time when I first started digging into generators.
The Usage File
USAGE file provides a home for the help content displayed when you run
bin/rails generate plain --help. (Figure 6)
plain is the name for the generator we’re building.) Depending on the complexity of your generator, the usage file may not be critical, but even with a single argument, it’s worth having the ability to surface quick reminders for yourself or your teammates.
It’s also worth noting that the
--help flag is optional. If you run the a generator without arguments, it will display the help by default. So the more info or examples in the usage file, the easier people will be able to use it. In addition to your usage file, Rails will automatically include the details about arguments and parameters based on the values defined in your generator by using the value you define in the
banner option. So you can think of the
USAGE file as the bonus content for you to provide a friendly description as well as specific examples of the way the command could be used with various options.
At this point, we don’t have any additional arguments, but it could be handy to support passing a list of attribute names similar to how the ActiveRecord
model generator does. We’ll save that for later, though. For now, we’ll stay focused on the basics with the default generator.
The Templates Folder
It’s initially empty, but the
templates folder serves as the home for the file templates that we’ll use to generate our files. If you take a look at the templates folder for the task generator, you’ll see the basic structure of a rake task with some ERb. You may also notice that the templates use the
tt extension which stands for “Thor Template”.
Finally, the generator also provides the test files for ensuring our generator does what we expect. (Figure 7) While tests are always important, with generators, the automated tests go far beyond that because manually testing and cleaning up any incorrectly-generated files is tedious and error-prone with even the simplest generators.
Let’s look at the starting point for our test file:
Lines five through seven are where the magic happens for testing generators. The
tests line lets it know what generator to run when you call
run_generator in the test. The
destination line ensures that files generated by the test don’t end up in your actual codebase where they could accidentally be committed to your repo. Instead, it puts them in the
tmp/generators folder where they won’t interfere.
And finally, the
setup line ensures that the destination folder is clean before each test. A
teardown method is also provided, but as long as the destination is prepared in the setup step, you shouldn’t need the teardown unless your tests generate some additional files that need to be cleaned up or your project isn’t setup to ignore the
tmp directory. If you don’t tear down the resulting files, it’s also easier to manually inspect the results after running a single test since the generated files will be untouched.
The safety and automated cleanup you get from these makes it much easier to build and test new generators without worrying about unintended consequences. As long as your generator uses the
destination_root method when specifying paths, your tests should only generate files in the specified test directory. For the most part, it’s as simple as using
Rails.root.join(destination_root, ...) any time you need to reference a folder. That will ensure that your generator uses the currently defined destination, and since your test specified that to be
tmp/generators, you’ll be all set both when testing and when running the command.
Rails also provides some generator-specific assertions to help make generator tests a little easier to create as well as easier to read after the fact. If you’re using RSpec, you’ll likely have to build some tooling to ensure a smooth test-writing experience.
Now that we’ve set the table with the default files and context, we’re ready to make the generator actually do something. Ultimately, we’re going to generate two files: the model file and the model test file. So we’ll start by creating the templates for those files. And remember, the
NamedBase class provides quite a few convenience methods.
Before we can have the generator do stuff, we have to provide the template files that serve as the source material for generating our final files. We’ll put all of these in the
lib/generators/plain/templates folder and tack on a
.tt (Thor Template) extension. The templates can use ERb and have access to all of the methods that
NamedBase provides. (Figures 8 & 9)
Now we have some bare bones templates. They don’t do much at this point, but that’s alright. We’ll enhance them later.
Automated tests are great, but automated tests for generators really help save time. So before we implement the generator, let’s add some tests based on our expected results from running the generator.
So far, it’s probably felt like a whole lotta setup, and you’re ready for something more substantial, and that’s what we’ll do next. Fortunately, at this point, the generator is fairly straightforward, and we’re creating a fairly simple generator. Before we do, though, there’s one more thing to know about generators: they will call all of the public methods in the generator in the order they’re defined.
source_root method establishes the generator’s local
templates directory as the source for the template files, we can use the template command from the Thor::Actions. With everything prepared, the
template method only needs two arguments to work: the name of the source file in the templates folder (without the
.tt extension) and the file path for the file you want generated after the ERb is parsed in the template. (Figure 11)
With everything else in place, you may want to take a moment to familiarize yourself with the available methods via the inheritance chain. We won’t use any of the others in our example, but even skimming the available methods can help reveal the capabilities of generators. I’ll include a few interesting methods from each right here, but it’s still worth reading the documentation to know all of your options. I’ve found that keeping these links handy helps when I want to make sure there’s not already a built-in way to do something where I might otherwise reach for
- Unified Summary of Rails and Thor Actions where I explored all of the actions available to Generators via both Thor and Rails
- Rails::Generators::Base which includes:
- Rails::Generators::NamedBase which provides convenient access to all of the standard Rails-based string inflection options like
With all of the tools and methods available through these options, you can perform some pretty robust manipulation of files and directories with concise, readable, and predictable commands. But let’s look at one more feature of generators that can help save time and then use that feature to make a minor enhancement to our generator.
Enhancing Our Generator
So far, we’ve kept our generator relatively simple so we could focus on how generators work rather than being distracted by what they’re generating, but where’s the fun in that? Passing arguments and options to generators is where things start to get really interesting.
Command Line Arguments
Our generator can receive a name via the command line, but it would be nice if it could accept some additional arguments or options and generate a few extra bits on our PORO. I’ve got two ideas in particular that be nice conveniences and give us an opportunity to see how to add arguments and options/flags.
First, we’ll update the generator to support an arbitrary number of arguments for attributes that we would like to have on our PORO. We’d like to be able to run something like this to add attributes onto our object:
So we need to specify that we’d like to capture the additional arguments that follow the
name argument. You can take a peek at the built-in Rails model generator to see how it handles the array of attribute fields. (Figure 13)
We want something fairly similar, but since our PORO isn’t database-backed, so we can skip the
index options used with standard model generator.
Seems straightforward enough, so let’s add the argument declaration to our generator. (Figure 14) Once that’s taken care of, our generator can capture the additional arguments, but it won’t yet be doing anything with them. Let’s see where we’re at, and then we’ll have the generator put the additional info to use.
Now that we can accept the arguments, what do we do with them? We head over to the model template file and iterate over the array of attributes to add the
attr_accessor declaration to our PORO, but before we do, let’s go ahead and update our tests so have a failing test prior to updating the template.
For the most part, we’ll be able to reuse our existing test for the model file, but now we need the test to run the generator with the attribute name arguments. (Figure 15)
Fortunately, that only requires adding additional values to the array passed to the
run_generator method. Then we need to update the content match assertion to expect our file to have the correct
Now we just need our model template to do the work and add the declaration, but since attributes are optional, we’ll need to remember to only add them if the generator parse any from the command line. While the argument name is
attributes, that provides a collection of
Rails::Generators::GeneratedAttribute values. While the instances of
GeneratedAttribute can be used via the
attributes, we’ll use the
attributes_names method that returns simply the names of the attributes passed to the generator. (Figure 16)
Now if you run your tests, they should be passing no problem. Our generator is still pretty basic though, and we need to touch on how to pass options/flags to it. So that’s what we’ll dig into next.
Command Line Options/Flags
In addition to arguments, Rails generators support options specified via flags thanks to Thor’s
class_options declaration. Values passed via class options can be of type string, hash, array, numeric, or boolean.
For our generator, we’ll keep it simple and use a boolean to support an
--no-active-model flag that will optionally auto-include ActiveModel.11One could extend this to support any subset of the Active Model modules, but we’ll keep it simple to focus purely on adding a single option to the generator. With that, we’d be able to automatically insert the
include ActiveModel::Model statement by default but support skipping it if our scenario doesn’t need it.
If all goes well, we could run something like the following:
But before we implement it, let’s go add tests to ensure we get it right. First, since we want to add ActiveModel as the default we’ll update the original test to verify that the include statement is present. (Figure 17)
We’re almost there. With the addition of one more line, we should have our first generator wrapped up. (Figure 18)
The Finished Generator
Now we have a generator for table-less plain old Ruby objects with some attributes and the ability to automatically include ActiveModel. It’s not the most advanced generator, but it certainly makes it easier to spin up simpler objects so we’re less tempted to bolt functionality onto ActiveRecord models.
I’ve made all the files available as a public Gist. If you notice any mistakes, opportunities for improvements, or suggestions for better approaches for creating custom Rails generators, I’d love to hear about it.