Shaping Rails to Your Needs, Customizing Rails Generators using Thor Templates

One of the most underrated features of Rails is its Generators. We use Generators in Rails all the time, every time we create a model, a controller, or a migration, we’re using Generators.

In most of our Rails apps, we may have a lot of code that we need to write repetitively.

NO! I’m not talking about reusable code that we can DRY out.

I’m talking about the logic that has already been extracted out to a module or a DSL method that needs to be added.

But did you know?, you can customize Rails Generators? You can! This is where Thor templates come into play.

Thor Templates

Thor Templates are files(.tt extension) that are used to automate the process of generating code. Rails Generators are built on top of Thor.

By creating our own Thor Templates, we can customize Rails Generators.

Creating a Thor Template

Our custom templates will reside under lib/templates/ directory.

Creating our custom templates is easy.

In this post, I’ll show two use cases for customizing Generators by creating our Templates.

Customizing ActiveRecord Model Template

Let’s assume we have a Rails app and we have a requirement that every Models (with few exceptions) should have the following features:

  • It should be searchable
  • It should be auditable
  • It should be able to retain its data even when deleted (Soft Deletable)

For these requirements, we have installed and integrated the following gems:

We now have the following logic in most of our models:

class User < ApplicationRecord
  include Discard::Model
  has_paper_trail
  
  searchkick callbacks: :async

  validates :name, presence: true

  def search_data
    {
      name: name,
      email: email
    }
  end
end

Now, we need to make sure these are added to every new model that we create in our app moving forward.

Let’s now create a custom template that helps us achieve this.

Let’s create a model.rb.tt file in lib/templates/active_record/model/ directory:

And add the following code:

<% module_namespacing do -%>
class <%= class_name %> < <%= parent_class_name.classify %>
  # FIXME: Remove Discard if SoftDeletion not required
  include Discard::Model
  # FIXME: Remove has_paper_trail if Audit is not required
  has_paper_trail

  # FIXME: Searchkick has been added Remove if not required
  searchkick callbacks: :async

  # FIXME: Remove the following code if name is not present
  validates :name, presence: true

  def search_data
    {
<% attributes.select{|a| [:string, :text].include?(a.type)}.each do |attribute| -%>
      <%= attribute.name %>: <%= attribute.name %>,
<% end -%>
    }
  end
end
<% end -%>

This template now ensures that every new model that we create will have the required features and we don’t need to add them manually.

So now if we run the following command:

rails g model Post title:string body:text

We’ll get the following output:

class Post < ApplicationRecord
  # FIXME: Remove Discard if SoftDeletion not required
  include Discard::Model
  # FIXME: Remove has_paper_trail if Audit is not required
  has_paper_trail

  # FIXME: Searchkick has been added Remove if not required
  searchkick callbacks: :async

  # FIXME: Remove the following code if name is not present
  validates :name, presence: true

  def search_data
    {
      title: title,
      body: body,
    }
  end
end

Now we can remove the name validation (since we don’t have a name attribute) and FIXME comments and we’re good to go.

Customizing Rails Migrations

In the above template, we have added a name validation as a common requirement for all models.

This means we are expecting a name attribute in all our models.

Let’s customize our migration template to add a name attribute to all our models.

We’ll create a create_table_migration.rb.tt file in lib/templates/migration/templates/ directory.

Inside this file, we’ll add the following code:

class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
  def change
    create_table :<%= table_name %> do |t|
      # FIXME: Remove the following code if name is not required
      t.string :name, null: false
<% attributes.reject {|a| ['name'].include?(a) }.each do |attribute| -%>
      t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
<% end -%>
      t.timestamps
    end
  end
end

Here, we have added a t.string :name, null: false line so that it generates a name attribute in all our models.

The rest of the code ensures all other attributes are generated as it normally does.

Now, let’s generate a new model and see the migration file:

rails g model Product description:text
class CreateProducts < ActiveRecord::Migration[7.1]
  def change
    create_table :products do |t|
      # FIXME: Remove the following code if name is not required
      t.string :name, null: false
      t.timestamps
    end
  end
end

The migration now has a name attribute added to it.

Observe that we didn’t specify it in the generator command, it was generated automatically by the template.

Now we can just remove the FIXME comment and run the migration.

Wrapping up

It makes a lot of sense to use custom generator templates if we have a lot of repetitive code in our app.

It saves us a lot of time and effort and makes our life easier.

There are other uses of templates that I have not covered in this post.

Adding a few references below for further reading:

Need help on your Ruby on Rails or React project?

Join Our Newsletter