This post focuses entirely on breaking down how templates work with Rails Generators. If you’re looking for a higher-level post that provides a more end-to-end guide, you may want to check out “Creating Custom Rails Generators” that zooms out to set up a broader understand without going as deep into specific topics.
The individual features of Rails Generators aren’t particularly fascinating by themselves, but the ways that Rails integrates all of the moving parts into a convenient package—including testing-makes it all much more interesting.
Somewhat ironically, though, in order to appreciate and be productive with your own custom Rails Generators, understanding the pieces in isolation can help demystify the big picture. So we’ll focus in on the templates functionality today. The core of the templates functionality originates from Thor, but Rails adds some magic and syntactic sugar to ensure they work smoothly in the context of Rails.
While templates are one of the more advanced parts of Rails Generators, conceptually, they’re relatively straightforward. (Figure 1) We have a source directory with some template files written in ERb, the generator itself, and a destination directory for the generated files.
template action can then be called from the generator where it combines the file system work and ERb parsing to create a more streamlined way to generate files. (Figure 2)
It relies on a single method with two parameters:
template "file.rb", "app/models/file.rb", but that’s somewhat deceptive as the Rails pre-configures the source and destination root values for Generators.
With that configuration, Generators have the knowledge to fill in the blanks and turn the ERb template into the desired destination file, but there’s some Rails (and Thor) magic that enables this. So we’ll break it down to understand how it’s all working so we can more efficiently work with Generators.
Pieces of the Puzzle
With templates, we have a handful of parts that make it all work seamlessly. We’ll briefly run through the pieces involved, and then we’ll dive into the details for each of them while covering some tips and tricks to ensure smooth operations.
First, the source (
source_root) and destination (
destination_root) directories provide predictable locations for the generator’s file system work. (Figure 3)
Next, the templates themselves provide the ERb support from within the generator’s “templates” folder in each generator’s source directory where Rails can recognize them as templates to be read and parsed into the resulting generated file.
template action serves as the glue within the generator by specifying the source template file name paired with the desired path for the generated file.
And finally, Rails provides some Generator-specific testing utilities help streamline verification without requiring a bunch of low-level file system cleanup before and after the tests.
Now that we’ve got context, let’s look at each of those pieces individually.
Source & Destination Directories
The templating functionality in Rails Generators relies on knowledge of the source and destination directories, and Rails creates an empty
templates directory as the default
source_root value each time it generates a new Generator. And if we look in a generator’s initial Class file, we see precisely one line of code setting the
source_root by pointing it at the
With that line, the dots are connected so we shouldn’t have to give a second thought about where the generator will look for the template files. You could set it to a different directory, but you shouldn’t need to for the vast majority of use cases. (Figure 5) But there’s still another dot that needs to be connected.
destination_root is the corollary to
source_root. By default, the destination root is set to
Rails.root, and in most cases, that’s what we want. The customizable
destination_root primarily comes in handy for automated testing in order to redirect the generated files to
/tmp—or somewhere equally disposable. That way we don’t have to worry about junk files accidentally being generated within the application or committed to the repository.
It may feel minor, but these source and destination settings play a key role in saving us from having to type something complex and unreadable to something much easier to type and read. (Figures 6 & 7)
On the surface, all of this may look like it only saves some typing, but it also makes that
template call much more readable. Without the directory and file extension obfuscating the template file name, we can more quickly recognize which file we’re referring to.
If you’re reading closely, you may have also noticed the
.tt extension in the first example is missing from the second. The
template method assumes the file extension so we don’t need to specify it either. (If you’re curious about that extension, hold that thought. We’ll get to it in the next section.) (Figure 8)
If you’re underwhelmed so far, I get it. This all looks relatively simple once you see how it works, but that’s the beauty of generators. They are that simple. The magic stems from how all of the conveniences and conventions add up and work together to make it all seamless.
Getting the Destination Right
For simplicity, I’ve used basic string concatenation for the destination path up to this point, but there’s one more thing worth keeping in mind. Let’s look at an example
template call for context. The source parameter is simple enough because it only needs the template file name, but the destination will be slightly more involved if we want to get it just right.
In this example, the destination path uses a
File.join call instead of pure string concatenation. This is one of those small things that ensures the code works on Windows, Unix, or Mac because
File.join uses the appropriate directory delimiter for the current system. (i.e.
\ on Windows and
/ on Unix/Macs.)
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.
Now that we’ve established some context on where templates live within generators, we can talk about the template files themselves, file names, ERb, and that
tt file extension.
File Type Extension
Any files in the templates folder need to have the
.tt (short for “Thor Template”) extension after the primary extension. But like we saw in the earlier example, the
.tt extension isn’t necessary in
template calls. Like the
source_root, the extension is handled automatically.
For example, with a file named
model.rb, Rails and Thor expect the template file to be named
model.rb.tt. The extension helps identify the file as a template, but more importantly, it ensures Rails never tries to autoload template files.
Of course, the magic of templates stems from supporting dynamic content through ERb. That content could be plain text, but the power stems from using ERb to make them dynamic and leverage Rails helper methods. For simplicity’s sake, you can think of any template file with the
tt extension as being ERb.
And since templates are processed in the context of the generator, any methods available within the generator are also available within the template files as well. That brings us to some of the string-oriented conveniences provided through Rails’ inflections.
With generators and templates, Rails’ inflections and supporting methods for the
name argument come in really handy. Any generators that inherit from
Rails::Generators::NamedBase also inherit a
name argument as the first command line argument by default. Rails then uses that value for a set of common string variants and inflections that streamline the ERb and file names.
With that generator command, we end up with
"Animal::Pet::Dog" as the
name attribute for the generator. (Figure 10)
Bear with me on the gratuitous use of namespaces because they help illustrate the robustness of the various inflections. Now we have access to a handful of methods that convert that name into a variety of useful formats to use in the generator and templates…
…and too many more to cover here, but the
Rails::Generators::NamedBase documentation covers them all. You may even recognize the patterns of those resulting strings because that’s how Rails does its thing when the built-in generators to create models, migrations, fixtures, tests, and controllers.
When necessary, we can also include the
Rails::Generators::ResourceHelpers module for a handful of additional controller-related convenience methods.
ERb Delimiter Escaping
For most file types, using ERb is straightforward, but things get a little meta when we need an ERb template to generate actual ERb. In that context, we need to escape the ERb delimiters (i.e.
%>) that we don’t want the generator to process.
To do that, we add an extra percent sign to the delimiter. So instead of
<% to open an ERb block, we use
<%%. The generator will process any unescaped ERb, and while processing it will convert the escaped delimiters to standard delimiters in the resulting file so we end up with working ERb.
Testing is the last part of the equation, and Rails provides some conventions and a handful of additional assertions for generators that simplify the process of verifying that the expected files exist and that their contents were generated correctly.
Discarding Generated Files
When we use the Generator generator, Rails automatically creates the test file and includes a line in the file that redirects the
tmp/generators so that the files generated during tests don’t end up in our repository. (Figure 11)
Rails also ensures that the destination files are cleaned up around each test so we never have to think about manually cleaning up leftover files after running tests.
This is another one of those little details that really helps make the process of creating custom generators run much more smoothly. And while you shouldn’t need to write that line yourself since it’s automatically generated, it’s worth noting that the method used is
destination rather than
destination_root. This is notable because
source_root is used to specify the source root value within the generator.
Assertions for Generators
With the test destinations sorted, let’s talk about the assertions Rails provides for testing generators. In particular,
assert_file (or its corollary
assert_no_file) will be one of the more common assertions, and it works several ways depending on the complexity of the generated file.
The versatility of
assert_file supports several different syntaxes. The first and simplest approach uses it to verify that a file exists in the destination without regard for the content. The second version lets us add a regular expression to verify some content exists in the file. And the third approach accepts a block to perform more-granular verification of the content. (Figure 12)
We can get pretty far testing generators with just those approaches, but there are some more specific assertions that we can use to verify the generated contents of a file.
assert_matchisn’t specific to generators, but it lets us use regular expressions or strings to verify that a specific string appears in the content of the block.
assert_instance_methodlets us verify that an expected method is correctly defined
assert_class_methodlets us verify that a class method is correctly defined, but it will not recognize class methods in a
class << selfblock.
When using one of the method assertions, we can pass a block that makes assertions about the contents of the method if it’s found. (Figure 13)
Relative to some of the other magic we get from Rails, generators feel less magical and just make sense. Rails provides some utilities that help with testing, and with some reasonable conventions around the source and destination paths, Rails implicitly knows a lot about your generators. This then reduces the amount of boilerplate code needed to test them quickly and completely.
This is only skimming the surface of testing generators, but we’ll cover testing more robustly in a separate post dedicated entirely to testing generators.
Templates represent one of the core features of Rails Generators, and while they’re relatively straightforward compared to other development tools, they wrap a fair amount of complexity under the hood so you don’t have to think about it.
Naturally, templates are just one of the tools that make generators so helpful. We’ll dig into the various actions provided by Rails and Thor, handling command-line arguments and options, testing, and more. I’ve also written a broader guide on custom rails generators if you’re looking for something more pragmatic that covers the full picture.