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
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.
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::Testing ::Assertions
- Rails::Generators::Testing ::SetupAndTeardown
Collect the Available Methods
The generator collected methods from each module using
private_instance_methods, and each class using
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
- Source file and line number via
parameters(including whether they’re optional or required)
- Aliases via
- Method Signature via
- 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.
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.