Magazine

DIY – Create Your Own Rails Generator

Generators are an essential tool to improve your workflow. In a few simple steps, we'll walk you through the process of creating your very own generator.

03 rails generator

If you have ever used Ruby on Rails for app development, you’ve surely used some of the many generators that come out of the box, such as the controller generator, migration generator, model generator, etc.

Generators are an essential tool to improve your workflow and in this article I will walk you through the process of creating your very own generator.

We won’t create just any random generator but a Service Generator.


What are service objects and why use them?

All of you are already using service objects, right? If not, you probably should... seriously!

Service objects are nothing but Plain Old Ruby Objects (PORO) that are used to execute actions.

1 (service) object == 1 responsibility

Service objects should be used to extract logic from controllers or models to keep them more readable and as slim as possible. They are great for keeping your code DRY and reusable. I won’t go into much details about them here, there are a lot of good articles on that topic.

Why wouldn’t we speed up our development and have a generator that does the boring job of creating the service skeleton, while we can focus on doing the fun stuff inside?

This is an example of how should a generated service look like:

class TestService
  def initialize
  end
  
  def call
  end
  
  private
  
  def method1
  end

  # .
  # .
  # .
  
  def methodN
  end
end

How to create custom generators?

There are 2 ways to create custom generators: manually and with generators. Both will do the trick, but I will use a generator to generate a generator.

Yes, you read that correctly. Rails generators themselves have a generator. So to create a generator, we could type in the terminal:

$ bin/rails generate generator service
      create lib/generators/service
      create lib/generators/service/service_generator.rb
      create lib/generators/service/USAGE
      create lib/generators/service/templates
      invoke test_unit
      create test/lib/generators/service_generator_test.rb

For Rails to find generator files, without writing extra autoload paths, we should put them in the lib directory. Because Rails is all about convention over configuration.

The command above will create a basic generator file:

class ServiceGenerator < Rails::Generators::NamedBase
     source_root File.expand_path('templates', __dir__)
end

The generator we are creating will inherit from Rails::Generators::NamedBase which basically means that our generator will expect at least the name argument to be sent. There are a lot more tricks and tips on creating generators so feel free to visit the related Rails guides for more info.


Implementing a custom Service Generator

Before getting into the code, here is my thought process. I wanted to create a generator that accepts an optional argument (an array of method names) and an optional option (module name that will namespace the class). The service generator would then generate the correct service file with a basic template that includes the pre-populated empty methods, if given. As we should try to only have one public method, the additional methods will be private. To build the generator I used Thor, a powerful toolkit for building command-line interfaces used by all Rails generators.

Let's proceed.

1. Create service generator

$ bin/rails generate generator service
      create lib/generators/service
      create lib/generators/service/service_generator.rb
      create lib/generators/service/USAGE
      create lib/generators/service/templates
      invoke test_unit
      create test/lib/generators/service_generator_test.rb

We use the built-in generator to create our own generator. The service_generator.rb file is the main file where we put our logic.

Let me walk you through the code.

source_root File.expand_path('../templates', __FILE__)

This method points to where our generator templates will be placed, and by default it points to the created directory lib/generators/service/templates.

argument :methods, type: :array, default: [], banner: "method method"
class_option :module, type: :string

Before we generate anything, we need to parse the command line arguments and options, if provided. That is done with the two methods provided above that come from Thor.

The method argument is used to parse command-line arguments into the methods variable and to create a attr_accessor for that variable while the class_option method is used to parse the command-line options and store them into the options variable.

The methods variable will be used in the template to generate empty methods while the options variable will be used to namespace the generator if the --module option is provided.

There is only one method inside our ServiceGenerator class called create_service_file where the logic is located.

@module_name = options[:module]

We store module name, if given, inside the instance variable.

The services directory should, according to the documentation, be placed inside the app directory, and that’s why the service directory path is stated as app/services, as this is our starting point.

service_dir_path = "app/services"
generator_dir_path = service_dir_path + ("/#{@module_name.underscore}" if @module_name.present?).to_s
generator_path = generator_dir_path + "/#{file_name}.rb"

We generate appropriate paths based on the module name, if present. The name of the service we want to generate is available for us to use in the class through the file_name variable that is accessible through the NamedBase class we inherit from. After all paths are generated, we can create directories if they don’t already exist. The template method is used to generate a file based on a template that we created in the path previously generated. More on this in the next step.

2. Create template

We could have used the create_file method for file creation in our service generator but I’ve used the template method because it is more flexible and it allows me to use the embedded Ruby file to dynamically change the file configuration based on user input. The file is a basic .erb file whose use case is to generate a class with initialize and call methods. If an additional option or argument is sent, then those changes are reflected in the file. Be sure to check the examples at the end.

3. Fill out the USAGE file

Lastly, we want to add information to our USAGE file to make it easier for other users. Information added in the USAGE section will be visible when the --help or -h option is sent alongside the command. You can fill this file based on what type of generator you are creating. For this case, let's add the following information:

So if you type:

 rails g service -h

the output will look like:


Examples

1. Basic service

rails g service test_service
class TestService
  def initialize
  end
  
  def call
  end
end

2. Service with additional methods

rails g service test_service test_method1 test_method2
class TestService
  def initialize
  end
  
  def call
  end
  
  private
  
  def test_method1
  end
  
  def test_method2
  end
end

3. Service with module name given

rails g service test_service --module test_module
module TestModule
  class TestService
    def initialize
    end

    def call
    end
  end
end

4. Service with additional methods and module name

rails g service test_service method1 method2 --module test_module
module TestModule
  class TestService
    def initialize
    end

    def call
    end

    private

    def method1
    end

    def method2
    end
  end
end

Cheers for sticking out till the end!

I’ve created a gem based on this code. You can check it out on GitHub.

This is my first blog article and I am open for any questions, suggestions, remarks and anything else, just leave a comment below!

If you wish to get in touch with me, you can reach out to me via Twitter or LinkedIn.

Leave a comment Be the first!