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:
- 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.
- Customization: By writing your generator script, you can add custom features, methods, or logic that aren’t provided by the built-in generators.
- 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 ๐
rails g generator custom_scaffold
It will generate the following files in lib
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.
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
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.
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.
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. 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
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.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.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.file_name
: Returns the file name for the resource, typically in snake_case. Example: If the first argument is Post, file_name returns post.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.human_name
: Returns a human-readable version of the resource name. Example: If the first argument is Post, human_name returns Post.i18n_scope
: Returns the I18n scope for the resource. Example: If the first argument is Post, i18n_scope returns activerecord.models.post.route_url
: Returns the default route URL for the resource. Example: If the first argument is Post, route_url returns posts_url.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.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
rails generate custom_scaffold Article
Destroying Generated Files ๐
Rails provides methods to destroy all the files generated by a generator.
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/
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
.
argument :attributes, type: :array, default: [], banner: "field:type field:type"
def create_scaffold
generate "rails:scaffold", "#{name} #{attributes.join(' ')}"
end
To run this use,
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:
bundle add pundit && rails g pundit:install
Add this method to the CustomScaffoldGenerator
to generate the pundit policy.
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:
orbefore
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.
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
.
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 ๐
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.