Learning
Unlocking the potential of Ruby on Rails custom generators

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 indexCode 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
endCode 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
endCode 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 dogsCode 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
endCode 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}'"
endCode 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
endCode 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
endCode 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 ducksCode 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
endCode 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.

Leave a Reply

Your email address will not be published. Required fields are marked *