See more posts

The Power of Rails Generators

One of the things that initially drew me to rails, aside from ruby itself was the ability to generate boilerplate code and build CRUD actions in a matter of seconds. The classic build a blog example.

Since then, a lot of teams that I've been on have had system patterns that would have been 10x faster to implement if they used a simple generator. A great candidate that I saw was a set of admin functions that were called to perform common or even one-off actions (like manually reset a user's password, invalidate caches, clean certain db records, etc.). Luckily Rails provides a generator generator rails g generator ... . That's great if you want a service object, form object, or component generator, for example.

The way that rails implements generators makes it easy to override the templates that it uses as well. The lookup path prioritizes local lib paths over the paths from the rails gems like active_record.

For example, you could add # frozenstringliteral: true to all your generated files, or null: false to the timestamps statement in migrations, or even add column annotations as comments to model files.

As an aside, if you use neovim, you can add combined language grammars for embedded ruby (eruby) and ruby in your init.lua file:

lua
vim.filetype.add({
  pattern = {
    [".*%.rb%.tt"] = "eruby.ruby",
  },
})

Here, I added a couple of things to the migration template like null: false, default: -> { "now()" } , and the VERSION string as a comment:

lib/templates/active_record/migration/create_table_migration.rb.tt

ruby
# frozen_string_literal: true

# Created at <%= Time.current %>
# VERSION=<%= migration_number %>
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
  def change
    create_table :<%= table_name %><%= primary_key_type %> do |t|
      <%- attributes.each do |attribute| -%>
        <%- if attribute.password_digest? -%>
      t.string :password_digest<%= attribute.inject_options %>
        <%- elsif attribute.token? -%>
      t.string :<%= attribute.name %><%= attribute.inject_options %>
        <%- elsif attribute.reference? -%>
      t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %><%= foreign_key_type %>
        <%- elsif !attribute.virtual? -%>
      t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
        <%- end -%>
      <%- end -%>

      <%- if options[:timestamps] -%>
      t.timestamps null: false, default: -> { "now()" }
      <%- end -%>
    end

    <%- attributes.select(&:token?).each do |attribute| -%>
    add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
    <%- end -%>
    <%- attributes_with_index.each do |attribute| -%>
    add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
    <%- end -%>
  end
end

And here, I added some annotations to the model generator:

lib/templates/active_record/model/model.rb.tt

ruby
# frozen_string_literal: true

<%- module_namespacing do -%>
# | column               | type       | index? | unique | null   |
# |----------------------|------------|--------|--------|--------|
<%- attributes.each do |attribute| -%>
# | <%=- attribute.name.ljust(20) -%> | <%=- attribute.type.to_s.ljust(10) -%> | <%= (attribute.has_index? ? "true" : "").ljust(6) -%> | <%=- (attribute.has_uniq_index? ? "true" : "").ljust(6) -%> | <%=- attribute.attr_options[:null].to_s.ljust(6) -%> |
<%- end -%>
class <%= class_name %> < <%= parent_class_name.classify %>
  <%- attributes.select(&:reference?).each do |attribute| -%>
  belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %>
  <%- end -%>
  <%- attributes.select(&:rich_text?).each do |attribute| -%>
  has_rich_text :<%= attribute.name %>
  <%- end -%>
  <%- attributes.select(&:attachment?).each do |attribute| -%>
  has_one_attached :<%= attribute.name %>
  <%- end -%>
  <%- attributes.select(&:attachments?).each do |attribute| -%>
  has_many_attached :<%= attribute.name %>
  <%- end -%>
  <%- attributes.select{|a| a.name.end_with?('token')}.each do |attribute| -%>
  has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %>
  <%- end -%>
  <%- if attributes.any?{|a| a.name == 'password_digest'} -%>
  has_secure_password
  <%- end -%>
end
<%- end -%>

Now if I run the model generator:

zsh
rails g model Company name:citext\!:uniq website bio:text

I get:

ruby
# frozen_string_literal: true

# Created at 2025-11-02 16:28:56 UTC
# VERSION=20251102162856
class CreateCompanies < ActiveRecord::Migration[8.0]
  def change
    create_table :companies do |t|
      t.citext :name, null: false
      t.string :website
      t.text :bio

      t.timestamps null: false, default: -> { "now()" }
    end

    add_index :companies, :name, unique: true
  end
end

app/models/company.rb

ruby
# frozen_string_literal: true

# | column               | type       | index? | unique | null   |
# |----------------------|------------|--------|--------|--------|
# | name                 | citext     | true   | true   | false  |
# | website              | string     |        |        |        |
# | bio                  | text       |        |        |        |
class Company < ApplicationRecord
end

Pretty cool!

Thanks for reading! See more posts Leave feedback