stretch-image no-lazy building_rails_scafford_generator

Creating Your Own Scaffold Generator in Rails

   Back to list

Starting a new project using Ruby on Rails is very easy. While with a lot of other frameworks you spend some time building a basic MVC-carcass, Rails does everything for you thanks to its built-in scaffolding feature. That’s so enjoyable and refreshing; one command and you have model, migration, controller, views, helpers, and even assets. A lot of gems will add more features, for example rspec-rails will generate all the specs you need (and some you don’t), and factory_girl_rails will add an empty factory for your model.

But after some time you notice that all the time you saved with Rails Scaffold Generator you now spend removing some generated files and heavily re-writing others. Luckily, Rails gives us tools to customise what will be generated with configuring and changing templates.

Still, copying configs and templates from project to project is not a very optimised workflow. It also makes your code messy because developer tools don’t belong in the /lib folder. The nice and ruby-compatible way is to put code in your own gem, and that’s what we will do today. Actually, let’s also create our own generator instead of patching the Rails’ one. This way we’ll have full control of what is generated and how.

The example that we will build is the actual generator I use in my projects. It generates only controller, views and controller spec for Rspec. It expects database table, model and model’s factory to be in place. It is also designed specifically for Devise and CanCanCan gems. By the way, we have a great article about them.

Creating Ruby Gem with basic generator

I will not go into details about creating gems. There’s very thorough official guides for gems and bundler. So after

$ bundle gem nopio_scaffold

which will generate gem structure for us, we have our gem which does absolutely nothing. Let’s add our extremely useful gem to the test rails application. Just add

gem 'nopio_scaffold', :path => '[path to gem folder]'

to your Gemfile and we’re ready to use it. Now we just need to write some code.

Note: we have two folders: one with gem and one with our rails application. All the code we write is for the gem. All the console commands we use further we call from the rails application folder.

Rails generators are based on Thor. But of course we’ll also want to use all the nice features that Rails team built on top of it. Let’s start with creating the main class for our generator:

[gem folder]/lib/generators/nopio_scaffold/controller_generator.rb

module NopioScaffold
  module Generators
    # Custom scaffolding generator
    class ControllerGenerator < Rails::Generators::NamedBase
      include Rails::Generators::ResourceHelper

      desc "Generates controller, controller_spec and views for the model with the given NAME."   
  end
end

Inheriting the class from Rails::Generators::NamedBase will give us all the Thor features with a lot of additional useful methods from Rails like #class_name, #singular_name etc. Including Rails::Generators::ResourceHelper will add a few more helpful methods, e.g. #controller_class_name.

If you open console in the root of your rails application folder and type

$ rails g

you’ll see the list of all the generators you have. There already should be:

NopioScaffold:
  nopio_scaffold:controller

Of course if you run it, it will do nothing.

Let’s also create our own helper class in the same folder. We will need it later:

[gem folder]/lib/generators/nopio_scaffold/generator_helpers.rb

module NopioScaffold
  module Generators
    # Some helpers for generating scaffolding
    module GeneratorHelpers

    end
  end
End

Don’t forget to include it in our generator’s main class:

[gem folder]/lib/generators/nopio_scaffold/controller_generator.rb

require 'generators/nopio_scaffold/generator_helpers'

module NopioScaffold
  module Generators
    # Custom scaffolding generator
    class ControllerGenerator < Rails::Generators::NamedBase
      include Rails::Generators::ResourceHelper
      include NopioScaffold::Generators::GeneratorHelpers

Using templates

To use templates we need to tell our generator where to look for them:

[gem folder]/lib/generators/nopio_scaffold/controller_generator.rb

source_root File.expand_path('../templates', __FILE__)

As you see, our first template goes to the templates folder:

[gem folder]/lib/generators/nopio_scaffold/templates/controller.rb

class <%= controller_class_name %>Controller < ApplicationController
  before_action :authenticate_user!
  load_and_authorize_resource
end

You can write templates as usual erb files. The result of this one will be our controller class. The controller will be empty, and will only have Devise and CanCan methods calls. Now we need to use this template.

[gem folder]/lib/generators/nopio_scaffold/controller_generator.rb

def copy_controller_and_spec_files
  template "controller.rb", File.join("app/controllers", "#{controller_file_name}_controller.rb")
end

We don’t need to call methods in the generator class. All public methods will be called one by one on generating.

Now we can finally run our generator:

$ rails g nopio_scaffold:controller book

This command should create a controller with class BooksController in the /app/controllers folder of our test rails application. Pretty cool, right?

Making result depend on model’s attributes

Now let’s add actions to our controller:

[gem folder]/lib/generators/nopio_scaffold/templates/controller.rb

  def index
  end

  def show
  end

  def new
  end

  def create
    if @<%= singular_name %>.save
      redirect_to @<%= singular_name %>, notice: '<%= human_name %> was successfully created.'
    else
      render :new
    end
  end

  def edit
  end

  def update
    if @<%= singular_name %>.update(<%= singular_name %>_params)
      redirect_to @<%= singular_name %>, notice: '<%= human_name %> was successfully updated.'
    else
      render :edit
    end
  end

  def destroy
    @<%= singular_name %>.destroy
    redirect_to <%= plural_name %>_url, notice: '<%= human_name %> was successfully destroyed.'
  end

As you see it’s pretty different from standard Rails scaffolding. For example #index is absolutely empty. That’s because CanCan’s #load_and_authorize_resource does a lot of work for us, and that’s one of reasons why I needed my own generator.

For the controller to be ready we also need to add a private method for parsing parameters (using params.require). But it would be really great for all params to already be there. Rails Scaffold Generator generates views based on an attributes list we send to the generator. Since our generator doesn’t accept attributes options as it expects models to already exist, we need to somehow get the fields list from that model. We have a helper module, let’s add some helper methods there:

[gem folder]/lib/generators/nopio_scaffold/generator_helpers.rb

  attr_accessor :options, :attributes

  private

  def model_columns_for_attributes
    class_name.constantize.columns.reject do |column|
      column.name.to_s =~ /^(id|user_id|created_at|updated_at)$/
    end
  end

  def editable_attributes
    attributes ||= model_columns_for_attributes.map do |column|
      Rails::Generators::GeneratedAttribute.new(column.name.to_s, column.type.to_s)
    end
  end

As you see we get all our attributes from the model, remove the ones that shouldn’t be edited and then generate an array of helpful objects. Now we can use this array in the controller template:

[gem folder]/lib/generators/nopio_scaffold/templates/controller.rb

  private

  def <%= singular_name %>_params
    params.require(:<%= singular_name %>).permit(<%= editable_attributes.map { |a| a.name.prepend(':') }.join(', ') %>)
  end

This particular part of the template will give us this result in the generated controller:

  private

  def book_params
    params.require(:book).permit(:name, :author, :year)
  end

Adding options to the generator

Sometimes for small models “show” action isn’t really needed, so we will add –skip-show option to our generator.

We can already pass some standard options to our generator, like -f, [–force] or -q, [–quiet]. We can add our own very easily:

[gem folder]/lib/generators/nopio_scaffold/controller_generator.rb

  class_option :skip_show, type: :boolean, default: false, desc: "Skip "show" action"

[gem folder]/lib/generators/nopio_scaffold/generator_helpers.rb

  private

  def show_action?
    !options['skip_show']
  end

Now we can use our new helper in the controller template:

[gem folder]/lib/generators/nopio_scaffold/templates/controller.rb

  def index
  end
<% if show_action? -%>

  def show
  end
<% end -%>

  def new
  end

Take a look at “-%>”. It’s pretty normal erb syntax, but rarely used in html templates. It specifies that a new string shouldn’t be started after erb block.

Creating templates for templates

We already had a model, now we have a controller. All that’s left is to add some views and we’ll have our MVC structure. Since we don’t need to think about other people using our generator, we can add some things that are not usually in scaffolding. For example bootstrap style classes.

“index.html.erb” view

[gem folder]/lib/generators/nopio_scaffold/templates/views/index.html.erb

Please notice how erb-blocks look when they should be in the resulting view:

<%= "<% end %%>" %>

“%%>” generates %> instead of closing the block.

“_form.html.erb” view

[gem folder]/lib/generators/nopio_scaffold/templates/views/_form.html.erb

In the _form.html.erb template we take advantage of more useful things than those we have in our editable fields list. For every attribute we generate an input field with a specific name and input type. As a result we will have a form that would need very few alterations comparing to an empty form from a standard generator.

Other templates are not so interesting, but here they are:

“show.html.erb” view

[gem folder]/lib/generators/nopio_scaffold/templates/views/show.html.erb

“new.html.erb” view

[gem folder]/lib/generators/nopio_scaffold/templates/views/new.html.erb

“edit.html.erb” view

[gem folder]/lib/generators/nopio_scaffold/templates/views/edit.html.erb

And here is how we will call the generation of views from our generator class:

[gem folder]/lib/generators/nopio_scaffold/controller_generator.rb

  def copy_view_files
    directory_path = File.join("app/views", controller_file_path)
    empty_directory directory_path

    view_files.each do |file_name|
      template "views/#{file_name}.html.erb", File.join(directory_path, "#{file_name}.html.erb")
    end
  end

As you see it uses another helper method:

[gem folder]/lib/generators/nopio_scaffold/generator_helpers.rb

  def view_files
    actions = %w(index new edit _form)
    actions << 'show' if show_action?
    actions
  end

Generating a file from several templates

Ruby code guidelines tell us to separate long files into several smaller ones. Controller spec files are not short in general, and their templates will be longer because of additional logic. So it would be better to divide this template and have specs for each action in a separate file.

[gem folder]/lib/generators/nopio_scaffold/generator_helpers.rb

  def all_actions
    actions = %w(index new create edit update destroy)
    actions << 'show' if show_action?
    actions
  end

  def controller_methods(dir_name)
    all_actions.map do |action|
      read_template("#{dir_name}/#{action}.rb")
    end.join("n").strip
  end

  def read_template(relative_path)
    ERB.new(File.read(source_path(relative_path)), nil, '-').result(binding)
  end

  def source_path(relative_path)
    File.expand_path(File.join("../templates/", relative_path), __FILE__)
  end

Now we can put our views in spec/actions and just call the helper method from the main controller_spec template.

But first let’s create another helper. If you have worked with rspec, you will remember that it generates a controller spec with the usage of valid_attributes and invalid_attributes hashes. But of course instead of adding actual hashes, it asks us to add them ourselves with skip() method.

We have already used our attributes list several times; we should use it now as well. Here’s a method that gets a hash with one attribute (string or integer) with two different values for it:

  def field_to_check_update
    @field_update_in_spec ||= if text_field = editable_attributes.find { |attr| attr.type == 'string' }
      { name: text_field.name, old_value: "'Just Text'", new_value: "'New Text'" }
    elsif numeric_field = editable_attributes.find { |attr| attr.type == 'integer' }
      { name: numeric_field.name, old_value: 1, new_value: 2 }
    else
      false
    end
  end

So finally, here’s our spec template:

[gem folder]/lib/generators/nopio_scaffold/templates/spec/controller.rb

require 'rails_helper'

RSpec.describe <%= controller_class_name %>Controller, type: :controller do

  before do
    @user = create(:user)
    sign_in @user
  end

  let(:not_authorized_<%= singular_name  %>) { create(:<%= singular_name %>, user_id: subject.current_user.id + 1) }
<% if field_to_check_update -%>
  let(:valid_attributes) { { <%= field_to_check_update[:name]  %>: <%= field_to_check_update[:old_value]  %> } }
<% else -%>
  let(:valid_attributes) { skip('Add a hash of attributes invalid for your model') }
<% end -%>
  let(:invalid_attributes) { skip('Add a hash of attributes invalid for your model') }

  <%= controller_methods 'spec/actions' %>
end

I won’t add all the action files, this article has enough code already. Let’s just see part of the “update” partial where we use our valid_attributes.

[gem folder]/lib/generators/nopio_scaffold/templates/spec/actions/update.rb

  describe 'PUT #update' do
    context 'with valid params' do
<% if field_to_check_update -%>
      let(:new_attributes) { { <%= field_to_check_update[:name]  %>: <%= field_to_check_update[:new_value]  %> } }
<% else -%>
      let(:new_attributes) { skip('Add a hash of new attributes valid for your model') }
<% end -%>

      it 'updates the requested <%= singular_name %>' do
        <%= singular_name %> = create(:<%= singular_name %>, user_id: subject.current_user.id)
        put :update, params: { id: <%= singular_name %>.to_param, <%= singular_name %>: new_attributes }
        <%= singular_name %>.reload
<% if field_to_check_update -%>
        expect(assigns(:<%= singular_name %>).<%= field_to_check_update[:name] %>).to eq(<%= field_to_check_update[:new_value] %>)
<% else -%>
        skip('Check if your field changed')
<% end -%>
      end
 it 'redirects to the <%= singular_name %>' do
        <%= singular_name %> = create(:<%= singular_name %>, user_id: subject.current_user.id)
        put :update, params: { id: <%= singular_name %>.to_param, <%= singular_name %>: valid_attributes }
<% if show_action? -%>
        expect(response).to redirect_to(<%= singular_name %>)
<% else -%>
        expect(response).to redirect_to(<%= plural_name %>_path)
<% end -%>
      end
    end
  end

Changing already existing files

Almost ready! But if we run the generator right now and try to see our controller, we will get a rails routing error. Rails give us an easy way to add resources to our routes.rb with the special helper #route.

[gem folder]/lib/generators/nopio_scaffold/controller_generator.rb

  def add_routes
    routes_string = "resources :#{singular_name}"
    routes_string += ', except: :show' unless show_action?
    route routes_string
  end

Time to generate again… No, we still can’t see controller. We don’t get an error, we’re just being redirected to the root_url. What’s the problem? We forgot about CanCan. Even after signing in we still don’t have the ability to view and manage our model. Unfortunately we don’t have any useful hooks for adding something to our abilities.rb, but we still can add text to existing files with #inject_into_file.

[gem folder]/lib/generators/nopio_scaffold/controller_generator.rb

  def add_abilities
    ability_string = "n    can :manage, #{class_name}, user_id: user.id"
    inject_into_file "#{Rails.root}/app/models/ability.rb", ability_string, after: /def initialize[a-z()]+/i
  end

The Result

Now the coding part is finally ready. Let’s see what we’ve got:

$ rails g model books user_id:integer title:string author:string year:integer
$ rake db:migrate
$ rails g nopio_scaffold:controller books

These 3 commands will create a basic CRUD for Books for us model that we won’t need to change as much as we would with usual Rails Controller Scaffold.

rails_form

show_ruby

You can find the resulting gem here.

Send this to a friend