# Building Custom Generators in Rails

- Canonical: https://haseebeqx.com/posts/building-custom-generators-in-rails/
- Published: 2024-07-28


In Ruby on Rails, A generator is a tool that helps create scaffolding (basic code structure). By default, Rails provides several built-in generators like `rails generate scaffold`, `rails generate controller`, etc. Many of the gems come with their own Rails generators too.

A custom generator is used to create a customized scaffolding for your application. Custom generators in Rails allow developers to create their code generators to streamline repetitive tasks and enforce consistency across the codebase.



## Benefits of Custom Generator

You might wonder why you'd need custom generators when Rails already provides a rich set of built-in tools. Here are some reasons:

1. **Consistency**: With a custom generator, you can enforce consistency across your application by creating the same structure and naming conventions for related models or controllers.
2. **Customization**: By writing your generator script, you can add custom features, methods, or logic that aren't provided by the built-in generators.
3. **Reusability**: Once you've created a custom generator, you can reuse it across multiple projects or within your team to ensure consistent code quality and structure

## Creating Custom Generator

Let's create one custom generator to learn more about it. In this tutorial, we will create a custom scaffold that generates the model, controller, specs, pundit policy, and services. This will make use of the existing generators in Rails to create models, controllers, and views. It will use the `pundit` gem to generate a pundit policy. This scenario makes it easy for me to walk you through all the major aspects of the code generator. I'm not here to discuss about the debate whether scaffolding is good or bad. Just know this debate exists.

### Step 1: Generate The Generator

```bash
rails g generator custom_scaffold
```
It will generate the following files in `lib`

```tree
generators
└── custom_scaffold
 ├── USAGE
 ├── custom_scaffold_generator.rb
 └── templates
```

### Step 2: Define The Generator

In `lib/generators/custom_scaffold_generator`, you can add logic to create files, directories, or modify existing files.

The default `custom_scaffold_generator.rb` will look like this.

```ruby
class CustomScaffoldGenerator < Rails::Generators::NamedBase
 source_root File.expand_path("templates", __dir__)
end
```

In this `source_root` is where it will look for templates. A template is a file with `erb` type templates that will be used to create the files.

Let's start by creating a service. Add the following method into `CustomScaffoldGenerator`
```ruby
def create_service
 template 'service.rb', "app/services/#{file_name}_service.rb"
end
```

The `template` method is used to define which template should be used for creation and under what name. The name of the method `create_service` is not important. You can create any number of files from a single method.

In our case let's say we want to generate separate services for create, update, and destroy. Replace the existing `create_service` method with this `create_services` method.

```ruby
def create_services
  %w[create update destroy].each do |action|
    @action = action
 template 'service.rb', File.join('app/services', class_path, "#{file_name}_#{action}_service.rb")
  end
end
```

### Step 3: Create The Template

The templates are simple ERB-styled files where we define which pages to replace. Create the file `service.rb.tt` with the following content.

```ruby
class <%= class_name %><%= @action.capitalize %>Service
  def self.call(*args)
    new(*args).call
  end

  def initialize(<%= @action == 'destroy' ? singular_name : "params" %>)
 @<%= @action == 'destroy' ? singular_name : "params" %> = <%= @action == 'destroy' ? singular_name : "params" %>
 end

 def call
 # TODO: Implement <%= @action %> logic
    OpenStruct.new(success?: true)
  end

  private

  attr_reader :<%= @action == 'destroy' ? singular_name : "params" %>
end
```

As in regular Rails templates, `class_name` is passed an argument. The template can access @action. This internally uses a gem called [Thor](http://whatisthor.com/). Please see its documentation to learn more about the templates.

The `class_name` is generated from the first argument Here are the other methods and equivalent code in rails. Here is a list of other useful arguments

1. `class_name`: Returns the class name for the resource, derived from the first argument.
 Example: If the first argument is Post, class_name returns Post.

2. `singular_name`: Returns the singular form of the resource name, typically in snake_case.
 Example: If the first argument is Post, singular_name returns post.

3. `plural_name`: Returns the plural form of the resource name, typically in snake_case.
 Example: If the first argument is Post, plural_name returns posts.

4. `file_name`: Returns the file name for the resource, typically in snake_case.
 Example: If the first argument is Post, file_name returns post.
5. `table_name`: Returns the table name for the resource, typically in snake_case and pluralized.
 Example: If the first argument is Post, table_name returns posts.

6. `human_name`: Returns a human-readable version of the resource name.
Example: If the first argument is Post, human_name returns Post.

7. `i18n_scope`: Returns the I18n scope for the resource.
Example: If the first argument is Post, i18n_scope returns activerecord.models.post.

8. `route_url`: Returns the default route URL for the resource.
 Example: If the first argument is Post, route_url returns posts_url.
9. `resource_name`: Returns the name of the resource in a human-readable form, usually capitalized.
 Example: If the first argument is Post, resource_name returns Post.
10. `resource_plural_name`: Returns the plural form of the resource name, typically in a human-readable form.
 Example: If the first argument is Post, resource_plural_name returns Posts.

### Step 4: Generate
To generate
```sh
rails generate custom_scaffold Article
```

## Destroying Generated Files

Rails provides methods to destroy all the files generated by a generator.

```sh
rails destroy custom_scaffold Article
```

## Using Existing Generators In Our Custom Generator

Rails comes with a default `scaffold` generator. It will generate the model, migrations, controller, and some tests with default code.

For example, the following will generate a basic implementation of an article with all the required forms, model migrations, and controllers/
```sh
rails generate scaffold Article name:string price:decimal
```

We can use this to generate a basic scaffolding. So that we don't have to write the code directly every time.

Let's make use of it in our generator. Add the following code into `CustomScaffoldGenerator`.

```ruby
argument :attributes, type: :array, default: [], banner: "field:type field:type"

def create_scaffold
 generate "rails:scaffold", "#{name} #{attributes.join(' ')}"
end
```

To run this use,

```sh
rails generate custom_scaffold Article name:string price:decimal
```

The `argument` method is used to pass attributes to the scaffold. We configured it to accept attributes as an array.

Inside the `create_scaffold` method, We used existing scaffold generator using `generate "rails:scaffold"`

Then we passed all the arguments as a single string value.

To remove this you might have to call `rails destroy scaffold Article` separately.

### Using Generators From Other Gems

Using generators from other gems is also straight forward. The Pundit is an authorization gem. Let's add the `pundit` gem generator to generate the policies.

First, Make sure it is in your `Gemfile` and is installed by running:

```sh
bundle add pundit && rails g pundit:install
```

Add this method to the `CustomScaffoldGenerator` to generate the pundit policy.

```ruby
def create_policy
 generate "pundit:policy", name
end
```

## Adding content into existing files

We can make use of `inject_into_file` to add content to existing files.

The `inject_into_file` method takes the following parameters:
* File path: 'config/routes.rb' - This specifies the file to be modified.
* Options: options like `after:` or `before` with a specific regex or string
* Content to inject: The block passed to inject_into_file contains the content to be injected into the file.

Pundit Gem is used for authorization. We need to call the `authorize` method before each action. This can generated. Let's add helpers in our generator to achieve this.

```ruby
def add_pundit_in_controller
  def add_pundit_in_controller
 inject_into_file "app/controllers/#{plural_file_name}_controller.rb", after: /before_action :set_#{singular_name}.*\n/ do
      "  before_action :authorize_#{singular_name}\n"
    end

 inject_into_file "app/controllers/#{plural_file_name}_controller.rb", after: "private\n" do
      "\n  def authorize_#{singular_name}\n" +
      "    authorize @#{singular_name} || #{class_name}\n" +
      "  end\n"
    end
  end
end
```

In the code above, The first `inject_into_file` adds `before_action :authorize_article` after the generated `before_action :set_article` callback using a regex.

The second `inject_into_file` simply adds the `authorize_article` method to the next line after the private keyword.

## Replacing Existing Content

To edit the existing code, We can make use of the `gsub_file` method.

The gsub_file method takes the following arguments:

* File path: The relative path to the file you want to modify.
* Pattern: The pattern to search for within the file.
* Replacement: The content to replace the matched pattern.

We have generated our service. But, to use it, we have to edit the controller and add it ourselves. We will edit the controller and replace existing controller methods with updated code.

Add the following method in the `CustomScaffoldGenerator`. 

```ruby
def use_service_in_controller
 gsub_file "app/controllers/#{plural_file_name}_controller.rb", /def destroy[\s\S]*?^  end$/m do
    <<-METHOD.strip_heredoc
 def destroy
 result = #{class_name}DestroyService.call(@#{singular_name})
 respond_to do |format|
 if result.success?
 format.html { redirect_to #{plural_name}_url, notice: '#{class_name} was successfully destroyed.' }
 format.json { render :show, status: :ok, location: #{plural_name}_url }
 else
 format.html { render :show, status: :unprocessable_entity }
 format.json { render json: result.errors, status: :unprocessable_entity }
 end
 end
 end
 METHOD
  end

  %w[create update].each do |action|
 gsub_file "app/controllers/#{plural_file_name}_controller.rb", /def #{action}[\s\S]*?^  end$/m do
      <<-METHOD.strip_heredoc
 def #{action}
 result = #{class_name}#{action.capitalize}Service.call(#{singular_name}_params)
 respond_to do |format|
 if result.success?
 format.html { redirect_to @#{singular_name}, notice: '#{class_name} was successfully #{action}d.' }
 format.json { render :show, status: :#{action == 'create' ? 'created' : 'ok'}, location: @#{singular_name} }
 else
 format.html { render :#{action == 'create' ? 'new' : 'edit'}, status: :unprocessable_entity }
 format.json { render json: result.errors, status: :unprocessable_entity }
 end
 end
 end
 METHOD
    end
  end
end
```
This will edit the create, update, and destroy methods. But now, The issue is the create, update, and destroy methods are not implemented in the existing services. With the knowledge so far. You will be able to add a sample code for separate services if needed. I would suggest creating three specific methods `create_create_service`, `create_update_service` and create `destroy_service` if you are doing that.

## Final code

```ruby
class CustomScaffoldGenerator < Rails::Generators::NamedBase
 source_root File.expand_path('templates', __dir__)

 argument :attributes, type: :array, default: [], banner: "field:type field:type"

  def create_scaffold
 generate "rails:scaffold", "#{name} #{attributes.join(' ')}"
  end

  def create_services
    %w[create update destroy].each do |action|
      @action = action
 template 'service.rb', File.join('app/services', class_path, "#{file_name}_#{action}_service.rb")
    end
  end

  def create_policy
 generate "pundit:policy", name
  end

  def add_pundit_in_controller
 inject_into_file "app/controllers/#{plural_file_name}_controller.rb", after: /before_action :set_#{singular_name}.*\n/ do
      " before_action :authorize_#{singular_name}\n"
    end

 inject_into_file "app/controllers/#{plural_file_name}_controller.rb", after: "private\n" do
      "\n def authorize_#{singular_name}\n" +
      "   authorize @#{singular_name} || #{class_name}\n" +
      " end\n"
    end
  end

  def use_service_in_controller
    %w[create update destroy].each do |action|
 gsub_file "app/controllers/#{plural_file_name}_controller.rb", /def #{action}[\s\S]*?^  end$/m do
        <<-METHOD.strip_heredoc
 def #{action}
 result = #{class_name}#{action.capitalize}Service.call(#{action == 'destroy' ? '@' + singular_name : singular_name + '_params'})
 respond_to do |format|
 if result.success?
 format.html { redirect_to #{action == 'destroy' ? plural_name + '_url' : '@' + singular_name}, notice: '#{class_name} was successfully #{action}d.' }
 format.json { render :show, status: :#{action == 'create' ? 'created' : 'ok'}, location: #{action == 'destroy' ? plural_name + '_url' : '@' + singular_name} }
 else
 format.html { render :#{action == 'destroy' ? 'show' : (action == 'create' ? 'new' : 'edit')}, status: :unprocessable_entity }
 format.json { render json: result.errors, status: :unprocessable_entity }
 end
 end
 end
 METHOD
      end
    end
  end
end
```

## Conclusion

Rails generators can streamline your development process. Automating repetitive tasks, allows you to focus more on the unique aspects of your application, improving both efficiency and consistency. Whether you are creating a new model, controller, or even a custom generator, understanding how to leverage this can make your development workflow more robust.

By customizing and creating your generators, you can enforce coding standards and project conventions across your team, ensuring that everyone is on the same page. This not only enhances productivity but also maintains the quality and maintainability of your codebase.

Remember, while generators are a great asset, they should be used thoughtfully. Always review and understand the code they produce, making adjustments as necessary to fit the specific needs and best practices of your project.
