Heading image for post: Let's Build a Generator

Let's Build a Generator

Profile picture of Tony Yunker

Generators are one of the many excellent tools that Rails provides. They can be used for a wide range of tasks, from plugging a gem into your app to scaffolding files. You can also build generators to meet your specific needs.

Generators are one of the many excellent tools that Rails provides. They can be used for a wide range of tasks, from plugging a gem into your app and setting up the initializer to creating database migrations, models, views, controllers, tests, and rake tasks. Gems include their own generators that you can use (like pundit's policy generator).

It can be easy to reach for LLMs and agentic workflows to scaffold out files and code, but I think there's still a place for leveraging purpose built tooling in your ecosystem of choice. Small, sharp tools, you know? Using DSLs like generators gives you a precise language to speak that removes ambiguity and results in consistent output.

The amount of boilerplate required in ruby isn't something I complain about, but let's be honest - there's still some boilerplate you have to write. Generators can significantly reduce this and increase consistency between generated classes.

Running bin/rails g model BlogPost title:string body:text author:references is going to generate the necessary migration, model, tests and factory. The migration and factory will contain all the fields I included in the command, and the generated model class has the belongs_to association.

You can also build your generators to meet your specific needs. It can be extremely handy for generating domain-specific scaffolding. And they're easy to create, too.

Creating our Generator

Let's create a generator for a specific type of Job, let's call them FooJobs (which is the class they'll inherit from). We'll want to scaffold out the class, inherit from FooJob, define an empty perform method, and scaffold a test file with a basic test plus a pending test to implement. The basic test will pass, and the pending test sets us up to get straight into implementation. Fairly basic stuff, but it's a good walkthrough to show what a generator can do.

Let's start by scaffolding our new generator. What better way is there to generate the scaffolding of a generator than with a generator!? Conveniently, Rails includes a generator generator for this purpose:

$ bin/rails g generator foo_job
    create  lib/generators/foo_job
    create  lib/generators/foo_job/foo_job_generator.rb
    create  lib/generators/foo_job/USAGE
    create  lib/generators/foo_job/templates
    invoke  rspec
    create    spec/generator/foo_jobs_generator_spec.rb

This gives us a couple of files and directories to work with.

USAGE contains the help text that is displayed when you run bin/rails g foo_job --help.

foo_job_generator.rb is where we define what the generator should do. In our case, we'll want to apply the template for our job class and write it to the appropriate file name in app/jobs/. We'll also do the same for the test file, applying the test file template and writing it to spec/jobs/. We'll get to the templates in a minute.

class FooJobGenerator < Rails::Generators::NamedBase
  source_root File.expand_path("templates", __dir__)

  def generate
    template("job.rb.tt", "app/jobs/#{file_name}_job.rb")
    template("job_spec.rb.tt", "spec/jobs/#{file_name}_job_spec.rb")
  end

  private

  def job_name = [class_name, "Job"].join("")
end

We'll invoke the generator by running bin/rails g foo_job NAME, where NAME is the name of the job we want to create. Rails::Generators::NamedBase gives us a few helper methods such as file_name (the camel case transformation of NAME) and class_name (the classify transformation of NAME) to make things a bit easier.

Next we'll need to create the template files. The templates use ERB via Thor template files (.tt). Note that we can add helper methods such as job_name in the generator and they are available in the template files.

lib/generators/foo_job/templates/job.rb.tt will look like this:

<% module_namespacing do -%>
class <%= job_name %> < FooJob
  queue_as :foo_queue

  def perform
  end
end
<% end -%>

And lib/generators/foo_job/templates/job_spec.rb.tt will look like this:

require "rails_helper"

RSpec.describe <%= job_name %>, type: :job do
  it "enqueues the job on the foo queue" do
    expect { 
      described_class.perform_later
    }.to have_enqueued_job(described_class)
      .on_queue(:foo_queue)
  end

  describe "perform" do
    pending "it does a thing"
  end
end

Now, with all this built, we can create a BarJob and it's accompanying tests by running bin/rails g foo_job Bar

# app/jobs/bar_job.rb
class BarJob < FooJob
  queue_as :foo_queue

  def perform
  end
end
# spec/jobs/bar_job_spec.rb
require "rails_helper"

RSpec.describe BarJob, type: :job do
  it "enqueues the job on the foo queue" do
    expect {
      described_class.perform_later
    }.to have_enqueued_job(described_class)
      .on_queue(:foo_queue)
  end

  describe "perform" do
    pending "it does a thing"
  end
end

Adding a Queue Option

Now let's add an option to the generator to specify the queue the job should run on - bin/rails g foo_job Bar --queue high or bin/rails g foo_job Bar --queue low.

We'll define a class_option called queue in the generator, and the value provided to --queue is added to the options hash.

class FooJobGenerator < Rails::Generators::NamedBase
  source_root File.expand_path("templates", __dir__)

  # NOTE: we add the class_option here
  class_option :queue, type: :string, required: false, default: "low"

  def generate
    template("job.rb.tt", "app/jobs/#{file_name}_job.rb")
    template("job_spec.rb.tt", "spec/jobs/#{file_name}_job_spec.rb")
  end

  private

  def job_name = [class_name, "Job"].join("")

  # NOTE: and we extract the option to a method here
  def queue_name = options[:queue]
end

Then, in the template files, we add the queue_as class method and update the test:

# app/jobs/bar_job.rb
<% module_namespacing do -%>
class <%= job_name %> < FooJob
  queue_as :<%= queue_name %>

  def perform
  end
end
<% end -%>
# spec/jobs/bar_job_spec.rb
require "rails_helper"

RSpec.describe BarJob, type: :job do
  it "enqueues the job on the <%= queue_name %> queue" do
    expect {
      described_class.perform_later
    }.to have_enqueued_job(described_class)
      .on_queue(:<%= queue_name %>)
  end

  describe "perform" do
    pending "it does a thing"
  end
end

And now running bin/rails g foo_job Bar --queue high produces:

# app/jobs/bar_job.rb
class BarJob < FooJob
  queue_as :high

  def perform
  end
end
# spec/jobs/bar_job_spec.rb
RSpec.describe BarJob, type: :job do
  it "enqueues the job on the low queue" do
    expect {
      described_class.perform_later
    }.to have_enqueued_job(described_class)
      .on_queue(:low)
  end

  describe "perform" do
    pending "it does a thing"
  end
end

Wrapping it all up

So there we have it - hopefully, this has convinced you of the usefulness of Rails generators and how simple it is to generate your own!

Photo by Wolfgang Weiser on Unsplash

More posts about Workflow rails

  • Adobe logo
  • Barnes and noble logo
  • Aetna logo
  • Vanderbilt university logo
  • Ericsson logo

We're proud to have launched hundreds of products for clients such as LensRentals.com, Engine Yard, Verisign, ParkWhiz, and Regions Bank, to name a few.

Let's talk about your project