Rails Generators are special, and I’ve come to believe they’re a massive source of untapped potential in Rails apps. Every Rails developer uses the built-in generators regularly, and saves time while avoiding tedious work. That’s great, but Rails also provides the ability to create custom generators. In the right context, that opens the door to even more time saved, and I’ve been exploring that idea in-depth lately.

Unfortunately—but also understandably—the Rails support for custom generators only opens the door. It doesn’t guide us beyond that the way it does for the more core functionality of Rails. Plus much of the Generator functionality is available to us—and thus documented—through Thor.


With so much functionality split across Thor and Rails::Generators and then further spread out within each of those, flipping between two sets of documentation and source code was error prone. So I built a generator to automatically inspect and document esoteric knowledge about the capabilities of Rails Generators.

Up until automating the process, I had been using a plain Rails app for poking and prodding generators manually in cases where the available documentation or source code wasn’t entirely clear. I have a couple of basic generators that served as convenient test harnesses to implement, observe, and understand the real-world behavior of generator functionality.

Exploring every method manually had become time-consuming, though. So I started iterating on a sample generator that could take a list of “interesting” classes and modules and use Ruby’s introspection capabilities to construct a CSV file (Figure 1) of all available methods. Then I used the collected data to fill in additional facets of each method that could help expose structure and patterns that wouldn’t be easily visible otherwise.

[A zoomed out view of a small fraction of a large spreadsheet of Rails Generator methods and technical data about each to show the approximate scale of the data.] A zoomed out view of a small fraction of a large spreadsheet of Rails Generator methods and technical data about each to show the approximate scale of the data.
Figure 1

The collected data made it much easier to group, sort, and explore methods in ways that helped expose the implicit structure and capabilities of generators.

↩︎

It turned into a fun dive into Ruby introspection. The generator itself is quick, dirty, and highly-focused on Thor and Rails Generators, but the approach and the resulting insights are more broadly applicable.

I primarily focused on the more user-facing modules and classes (including test-related parts) from both Thor and Rails because my ultimate goal is to create a convenient quick reference.

Here’s the approach I took…

Decide what to Inspect

This involved looking through the docs and source code to determine where most of the user-facing functionality is defined. The process focused on the modules and classes that seemed most interesting and relevant in the context of basic custom generators without too much regard for expectations about each class or module individually. The goal was to uncover the unexpected rather than regurgitate the common knowledge.

Those classes and modules end up being:

  • Rails::Generators::NamedBase
  • Rails::Generators::Base
  • Rails::Generators::TestCase
  • Rails::Generators::Testing ::Assertions
  • Rails::Generators::Testing ::SetupAndTeardown
  • Rails::Generators::GeneratedAttribute
  • Rails::Generators::ModelHelpers
  • Rails::Generators::ResourceHelpers
  • Rails::Generators::Actions
  • Thor::Actions
  • Thor::Group
  • Thor::Base
  • Thor::Base::ClassMethods
  • Thor::Invocation
  • Thor::Shell
  • Thor::Shell::Basic
  • Thor::Shell::Color

Collect the Available Methods

The generator collected methods from each module using public_instance_methods, protected_instance_methods, private_instance_methods, and each class using singleton_methods and methods in order to build a list of the available methods to inspect.

Overbuild a Dummy a Generator

This was an interesting challenge because I wanted to collect real samples of data with a working generator, and since that included the test-related stuff, I had to require/include the test modules directly in the generator.

It also meant creating examples of each type of argument and option with various configurations in order to cover the spectrum of ways that generators can handle arguments and options.

Inspect each Method

With each available method, it created a method instance using the method method. i.e. generator.method(:template). With the instance of each method, it was simple to collect a few key pieces of information.

  • Defining Module/Class via owner
  • Source file and line number via source location
  • Method parameters (including whether they’re optional or required)
  • Aliases via original_name
  • Method Signature via to_s
  • Record whether it’s public/protected/private and whether it’s a singleton/class/instance method

Capture Return Values if Possible

Using the knowledge about each method, the generator would attempt to call any method that didn’t have required parameters and record the results to capture example values for inflection methods and available configuration values. i.e. class_name, human_name, route_url, etc. This also exposed all additional configured values available to generators beyond the standard inflections.

Import Documentation from Source Code

It used the source_location from the method to attempt to capture documentation from the source code if there was any for the method.

Approximate the Usage Frequency

It uses a very basic tokenization search through the source code for the built-in generators to count the uses of each method. It’s smart enough to ignore any instances of the methods in comments, but otherwise, it was naive and meant to be a signal rather than a precise record of frequency. It served primarily as good signal of the relative usefulness of the methods, but it’s definitely not accurate enough that I’d base anything objective on the results.

Create Rough Groupings

Once the data was gathered, it was easier to start seeing patterns and define about ten categories that the methods fit neatly into. I added some logic that looks at all of the available data for each method to roughly classify methods based on how they would normally be used. i.e. Actions, Inflections, Arguments, Options, Testing, etc.

Add a Rough Weight for “Interesting-ness”

Finally, it would score all of the available information for each method to generate a very rough approximation of “interesting-ness” or relevance based on some more subjective criteria.

  • Is it in one of the core classes/modules?
  • Does it have documentation?
  • Is it one of the more commonly-used methods?
  • Was it able to generate a sample return value?
  • Was it a public method?
  • Was it an instance method?
  • Was it an action or assertion?

With all of this data in a CSV, I was able to quickly tinker with grouping and sorting in a spreadsheet to explore the results and a get a feel for the overall shape of the functionality available to generators.

It also provided an exhaustive reference to help me uncover interesting functionality available to generators without flipping back and forth between source code, guides, and docs. And finally, it was a short jump to also create a YAML file that I could use for sharing aspects of the findings in blog posts.11This site uses a custom-built hybrid of Markdown + ERb so I can mix prose and programming.

And, most importantly, I wanted to be able to look at Generators from different angles in order to uncover different structures, relationships, and patterns. As it turns out, that came in even more handy than I ever imagined. This work enabled a really deep dive into Rails Generator Actions, and I have several more planned to explore other parts of generators.

This was one of those projects where I started out thinking it would be a quick way to turn twenty hours of manual labor into one hour of hacking, but instead it turned into twenty hours of hacking to uncover more details than I ever imagined. The end result is that I now have much more insight across both the Thor and Rails side of generators, and I’ll hopefully be able to feed a lot of that learning back into the official documentation.