DIY — Create Your Own Rails Generator

Ivan Matas
Ars Futura
Published in
6 min readFeb 12, 2019

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_sgenerator_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_method2class TestService
def initialize
end

def call
end
private def method1
end
def 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_modulemodule 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.

Originally published at arsfutura.co.

--

--