Shaping Rails to Your Needs, Customizing Rails Generators using Thor Templates
source link: https://blog.saeloun.com/2023/05/31/customizing-rails-generators-using-thor-templates/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Shaping Rails to Your Needs, Customizing Rails Generators using Thor Templates
May 31, 2023Keshav Biswa
Keshav is a Ruby on Rails Developer.
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:
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK