![Unlocking the potential of Ruby on Rails custom generators Unlocking the potential of Ruby on Rails custom generators](https://gerardomiranda.dev/blog/wp-content/uploads/2021/11/Ruby_On_Rails_Logo-1024x387.png)
Unlocking the potential of Ruby on Rails custom generators
In the last weeks I have been using mason to generate some boiler plate code for my personal project, may have to write about it some other time. So the idea of templating was sitting in my head at all times.
Then at my job I got assigned a task, a very common task actually, a third party integration. So my first thought was, I can use mason to generate some templates and make all our data processors consistent. Very excited, I went to ask for permission to create a new repository where I was already imagining filling it up with bricks (bricks are templates that mason uses to create your code). But the lead dev asked “doesn’t Rails have a feature for that?”.
At first I was thinking, “oh no! they don’t get it”. But after the initial reaction, it actually sound like a pretty good idea, so I went on to investigate about Rails generators, to see if we could use them for this case. The official docs didn’t have a very complete example, so I started looking for some blog posts and the API reference as well. I didn’t find any comprehensive guide, so I went on to experiment with it, I can show you a bit of the work I accomplished with it (not the real thing obviously) in case you need a more concrete example of how to create Rails generators.
Preparing
First let’s build something simple, like a service for getting animal pictures, there we are going to have options to show pictures from different kinds of animals.
rails g controller AnimalPictures index
Code language: Bash (bash)
Since cats are my favorite animals we will start with pictures of cats, lets add an image_tag and a button to retrieve the cat picture. Don’t forget to add the route for our post method get_cat, and in the controller we are going to retrieve a cute cat image I found with google.
animal_pictures/index.html.erb
<h1>AnimalPictures#index</h1>
<% unless @image_url.nil? %>
<%= image_tag @image_url, size:"512x512" %>
<% end%>
<%= button_to "Cats", action: :get_cat %>
Code language: ERB (Embedded Ruby) (erb)
config/routes.rb
get 'animal_pictures/index'
post 'animal_pictures/get_cat'
Code language: Ruby (ruby)
animal_pictures_controller.rb
class AnimalPicturesController < ApplicationController
def index
@image_url = params[:image_url]
end
def get_cat
redirect_to action: :index, image_url: 'https://i.natgeofe.com/n/548467d8-c5f1-4551-9f58-6817a8d2c45e/NationalGeographic_2572187_square.jpg'
end
end
Code language: Ruby (ruby)
With this we get a button that when pressed will show the picture, but wait… it always shows the same picture. To remedy that we will build a little client to call an API that will provide a different picture every time:
app/services/cats/client.rb
module Cats
class Client
include HTTParty
base_uri 'https://api.thecatapi.com'
def initialize
end
def get_picture_url
response = self.class.get('/v1/images/search')
response.parsed_response[0]['url']
end
end
end
Code language: Ruby (ruby)
And now we modify our get_cat method to use this API
animal_pictures_controller.rb
def get_cat
client = Cats::Client.new
redirect_to action: :index, image_url: client.get_picture_url
end
Code language: Ruby (ruby)
Building a basic generator
Our cat picture fetching service is now working, but remember the service was suposed to be for animals not only cats. Let’s add the option to retrieve dog pictures. But before that, you will notice, the client is probably something we will repeat a lot so it is a good candidate for making a generator (Finally!).
Let’s generate a generator:
rails generate generator AnimalApiClient
This will generate some files under lib/generators. First we are going to create a file in the templates folder and call it client.rb.tt where we are going to copy the code from the cats client and make a few modifications, removing some specific data and using the class_name method to name our module. Think of this like something similar to the erb files that render our views. Check all the methods provided by the NamedBased class that you can use to generate the file.
lib/generators/animal_api_client/templates/client.rb.tt
module <%= class_name %>
class Client
include HTTParty
base_uri ''
def initialize
end
def get_picture_url
response = self.class.get('')
# return the url
end
end
end
Code language: Ruby (ruby)
That is going to be our template, and now let’s move on to the actual generator that is going to use this template to generate our new clients.
lib/generators/animal_api_client/animal_api_client_generator.rb
class AnimalApiClientGenerator < Rails::Generators::NamedBase
source_root File.expand_path('templates', __dir__)
def create_client_file
template "client.rb", File.join("app/services", file_name, "client.rb")
end
end
Code language: Ruby (ruby)
You can define all the methods you want in the generator class, they will be called in the order they are defined.
Using the generator
We can now call the generator to create new clients, let’s create a new client, this time to retrieve picture of dogs:
rails g animal_api_client dogs
Code language: Bash (bash)
That will create the file app/services/dogs/client.rb with the following content:
module Dogs
class Client
include HTTParty
base_uri ''
def initialize
end
def get_picture_url
response = self.class.get('')
# return the url
end
end
end
Code language: Ruby (ruby)
Now we just have to update the base_uri, the endpoint and return the appropriate data, we will use shibe API. Next don’t forget to add the route, the button in the view and the method in the controller:
# config/routes.rb
post 'animal_pictures/get_dog'
# app/views/animal_pictures/index.html.erb
<%= button_to "Dogs", action: :get_dog %>
# app/controllers/animal_pictures_controller.rb
def get_dog
client = Dogs::Client.new
redirect_to action: :index, image_url: client.get_picture_url
end
Code language: Ruby (ruby)
Beyond the basics
Our project now can retrieve pictures of cats and dogs. And although replicating the dogs part was easier, I still have to modify another 3 files with repetitive code! We can make it even easier. Let’s add something else to our generator, we will insert some code into our existing files
route
You can add routes to your routes.rb file using the route method. In this case we will add a post route using the singular_table_name method to name the endpoint:
def add_routes
route "post 'animal_pictures/get_#{singular_table_name}'"
end
Code language: Ruby (ruby)
inject_into_class
For the code in the controller we are going to take another approach, we will use the inject_into_class method, that puts whatever content we provide just after the class declaration, then again we will be using the singular_table_name and class_name helper methods
def add_to_controller
inject_into_class "app/controllers/animal_pictures_controller.rb", AnimalPicturesController, <<~RUBY.indent(2)
def get_#{singular_table_name}
client = #{class_name }::Client.new
redirect_to action: :index, image_url: client.get_picture_url
end
RUBY
end
Code language: Ruby (ruby)
insert_into_file
Finally for our view we will use the insert_into_file method, this will allow us to specify an anchor for our content with the before: or after: options. In our case to facilitate our work we are going to modify our view and wrap our buttons in a div with a unique id, this div will serve as the anchor.
app/views/animal_pictures/index.html.erb
<div id="actions">
<%= button_to "Cats", action: :get_cat %>
<%= button_to "Dogs", action: :get_dog %>
</div>
Code language: ERB (Embedded Ruby) (erb)
Now let’s write the method in our generator:
def add_to_view
insert_into_file "app/views/animal_pictures/index.html.erb", after: '<div id="actions">' do <<~ERB.indent(4)
<%= button_to "#{class_name}", action: :get_#{singular_table_name} %>
ERB
end
end
Code language: Ruby (ruby)
This code will insert the button right after the opening div tag.
Let’s run our generator again, this time we will be fetching duck pictures
rails g animal_api_client ducks
Code language: Bash (bash)
We will have this result:
# config/routes.rb
Rails.application.routes.draw do
post 'animal_pictures/get_duck'
get 'animal_pictures/index'
post 'animal_pictures/get_cat'
post 'animal_pictures/get_dog'
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
# app/controllers/animal_pictures_controller.rb
class AnimalPicturesController < ApplicationController
def get_duck
client = Ducks::Client.new
redirect_to action: :index, image_url: client.get_picture_url
end
def index
@image_url = params[:image_url]
end
def get_cat
client = Cats::Client.new
redirect_to action: :index, image_url: client.get_picture_url
end
def get_dog
client = Dogs::Client.new
redirect_to action: :index, image_url: client.get_picture_url
end
end
# app/services/ducks/client.rb
module Ducks
class Client
include HTTParty
base_uri ''
def initialize
end
def get_picture_url
response = self.class.get('')
# return the url
end
end
end
Code language: Ruby (ruby)
app/views/animal_pictures/index.html.erb
<h1>AnimalPictures#index</h1>
<% unless @image_url.nil? %>
<%= image_tag @image_url, size:"512x512" %>
<% end%>
<div id="actions">
<%= button_to "Ducks", action: :get_duck %>
<%= button_to "Cats", action: :get_cat %>
<%= button_to "Dogs", action: :get_dog %>
</div>
Code language: ERB (Embedded Ruby) (erb)
Now we only have to add the specifics for the API we are going to use and it will be READY!.
Final thoughts
I hope this helped you know the capabilities of the Rails generators, especially when you have an application that requires a lot of integrations, or new features that have a very similar starting point. This may save you a lot of time. Complete code is in my repo.