loading...
Cover image for Understanding and writing Rails generators

Understanding and writing Rails generators

vinistock profile image Vinicius Stock ・6 min read

What generators are

Rails generators are command line tools that are used for automating the process of creating or editing files with boiler plate code.

In essence, they execute Ruby code much like a script and create or update files based on templates, user input and whatever logic necessary.

If you've played around with Rails, surely they are not a completely foreign concept to you. Many useful generators come bundled with Rails.

The model generator produces all files needed for a new model. It accepts a list of attributes and their respective types and generates files for the model, fixtures, migrations and unit tests.

Another example is the controller generator. It spins up a new controller with a specified list actions - adding their respective routes to config/routes.rb.

Their purpose is to automate writing repetitive code that you would otherwise need to memorize or copy. Not only that, they help teams enforce standards for writing specific implementations as well as speeding up development.

For a list of all generators defined within a given application, invoke generate (alias g) with no arguments. This includes generators from gems and from your own code base.

$ bin/rails g

Let's dig a bit more into generators and then learn how to write a custom one for our own needs.

We will split generators into two types: repeated use and one-off generators. While these names are not official, they will help convey the conceptual applicability of each one of them.

Repeated use generators

Repeated use generators are meant to be used multiple times whenever a developer is doing a specific task. The model generator mentioned earlier is a great example. A developer will create many models while working on an application and the generator can be used to achieve it every time.

Let's examine an example on invoking the model generator.

$ bin/rails generate model User name age:integer group:references

invoke  active_record
    create    db/migrate/20191002201613_create_users.rb
    create    app/models/user.rb
    invoke    test_unit
    create      test/models/user_test.rb
    create      test/fixtures/users.yml

This is how this command can be broken down.

  1. bin/rails invokes the Rails executable CLI
  2. generate is an argument for the Rails CLI telling it to invoke a generator
  3. model is an argument for the generate command and represents the name of the generator
  4. User is the first argument of the model generator, which is the name of the new model
  5. Finally, all the other arguments are attributes with their respective types for the model generator: my_attribute:my_type. Omitting the attribute type defaults to a string

Repeated use generators are great for everyday tasks that simply require certain standards or are not worth memorizing.

One-off generators

One-off generators are procedures that need to run a single time to prepare a code base for some functionality. Typically, they are used to automate project configuration and gem installation, since these changes only need to be applied once.

Gems or frameworks that require some sort of configuration in place to work properly will often provide these as install generators.

A great example is the authentication framework Devise, which provides install generators to get an application ready to use its authentication features.

For a shorter example (and a shameless plug), let's take a look at how to use the install generator of a gem of my own: Sail. It is an engine for live configuration of applications and requires three things to work properly: a table for settings, a table for profiles and a config YAML file for declaring settings.

# user@~/workspace/my_awesome_rails_app

$ bin/rails g sail:install
      create  db/migrate/20191009202801_create_sail_settings.rb
      create  db/migrate/20191009202802_create_sail_profiles.rb
      create  config/sail.yml

After running the migrations, the app is ready to take advantage of all features provided by the engine. This type of generator is an outstanding way of providing a smooth experience for developers that want to use your gem.

Instead of writing long docs on how to install your gem, consider providing an install generator that will set up their project for them. We can even make the generator ask for input from the developer to properly tailor the result to their needs.

Creating a custom generator

For our convenience, one the generators bundled in Rails is the generator generator. That's right, a generator for creating other generators (after all, who would want to memorize all the boilerplate code for that?). Let's imagine we want to create a component generator.

bin/rails g generator Component
      create  lib/generators/component
      create  lib/generators/component/component_generator.rb
      create  lib/generators/component/USAGE
      create  lib/generators/component/templates
      invoke  test_unit
      create    test/lib/generators/component_generator_test.rb

Let's zoom in into each file and explore why they exist.

Usage

This file defines the usage instructions that will be displayed when invoking the generator with no arguments. This is how it looks.

Description:
    Explain the generator

Example:
    bin/rails generate component Thing

    This will create:
        what/will/it/create

We can change the copy to explain what the generator is supposed to do and what developers can expected from it. Invoking the generator with no arguments will then print the information we wrote.

For our example, we will create a component generator that accepts the name of the component.

$ bin/rails g component

Description:
    Creates a component class

Example:
    bin/rails generate component MyComponent

    Creates the component class and unit test:
        app/components/my_component.rb
        test/components/my_component_test.rb

Component generator and its templates

The lib/generators/component/component_generator.rb file defines the class that does all the magic. Here you can write whatever Ruby code is convenient to produce the output desired.

This class automatically runs every method defined in it, so you can think that each method you define is a step of the generator. Not only that, every method defined will also be available for the generator's templates.

Let's look at the generated Ruby file.

# lib/generators/component/component_generator.rb

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

Currently, all the ComponentGenerator does is define the source_root as the templates folder. Let's add a few things to it:

  1. The main step of evaluating the component template and creating the resulting file
  2. An example method that can be invoked from the template. In this case, it just returns a string
  3. A class collision check to prevent the accidental creation of files with a duplicate suffix (e.g.: MyComponentComponent)
# lib/generators/component/component_generator.rb

class ComponentGenerator < Rails::Generators::NamedBase
  source_root File.expand_path("templates", __dir__)

  check_class_collision suffix: "Component"

  def create_component_file
    # Template method
    # First argument is the name of the template
    # Second argument is where to create the resulting file. In this case, app/components/my_component.rb
    template "component.rb", File.join("app/components",  "#{file_name}.rb")
  end

  private

  # Example method that can be invoked from the template
  def my_method
    "some string"
  end
end

And now let's create a template inside the templates folder. Again, notice how the methods we add to our generator class are accessible to the template (including the ones that are defined by the parent class Rails::Generators::NamedBase).

# lib/generators/component/templates/component.rb.tt

class <%= class_name %>Component
  def initialize
    @component_identifier = "<%= my_method %>"
  end
end

At this point, our generator is working and can create components with the desired name. It also makes use of our method that returns a string.

Running
$ bin/rails g component Post

Results in
# app/component/post_component.rb

class PostComponent
  def initialize
    @component_identifier = "some string"
  end
end

The last file created for us is a unit test for the generator itself. Rails provides a parent class for generator unit tests which contains a variety of helper methods for verifying the functionality we wrote.

Basically, we can assert which files are created and match against their contents.

class ComponentGeneratorTest < Rails::Generators::TestCase
  def test_component_is_created
    run_generator ["my_component"]
    assert_file "app/components/my_component.rb" do |component|
      assert_match(/class MyComponent/, component)
    end
  end
end

Next steps

There's a lot more that we can do with generators. Accepting arguments with specific types, asking for user input as the generator is running and test framework hooks to create unit test files for our component while respecting the testing library of choice (e.g.: minitest vs rspec).

I believed that including it all in a single article could be too dense. If the feedback is positive, I can turn this post into a series and explore more generator functionality. Please, let me know what you think in the comments!

Posted on Nov 29 '18 by:

vinistock profile

Vinicius Stock

@vinistock

Dev @ Shopify. Ruby & Rails open source contributor

Discussion

markdown guide