Creating your own generators in rails.

In this article I will show how to create a generator that generates the code from my previous article about basic user authentication in rails.

Rails generators are used to automatically generate code. You probably already use them to generate controllers, models and migrations. Every time you call script/generate you’re using a generator. However it’s easy to create your own generator in rails. If you find yourself copying code between applications you may find it useful to create a generator to generate the code for you.

Note: If your code is suitable for packaging as a plugin that is probably a better way to share the code between applications. However there are some cases when a generator is more suitable. Generators are useful for generating repetitive boiler-plate code or starting-point code that will be heavily modified. In this example the generator can be used to generate a basic authentication system for your application that you can then modify to suit the particular needs of your application.

As is usual in rails, creating a generator follows certain conventions. Create a folder for the generator (we call call this folder basic_auth). In this folder there should be a directory called templates. This directory holds all the code templates for our code generator. You can organize the files within this directory in whatever way you want. There should also be files called USAGE and basic_auth_generator.rb. The contents of the USAGE file are printed automatically when the user calls the generator with -h or —help as an argument.

The file basic_auth_generator.rb is the main file for the generator and contains a list of instructions for generating the target code. The full directory structure for the basic authentication generator code is:

basic_auth
|-- USAGE
|-- basic_auth_generator.rb
`-- templates
    |-- INSTALL
    |-- controllers
    |   `-- user_controller.rb
    |-- lib
    |   `-- basic_auth.rb
    |-- migrate
    |   `-- create_users.rb
    |-- models
    |   |-- notifications.rb
    |   `-- user.rb
    |-- test
    |   |-- fixtures
    |   |   `-- users.yml
    |   |-- functional
    |   |   `-- user_controller_test.rb
    |   `-- unit
    |       `-- user_test.rb
    `-- views
        |-- notifications
        |   `-- forgot_password.rhtml
        `-- user
            |-- change_password.rhtml
            |-- forgot_password.rhtml
            |-- hidden.rhtml
            |-- login.rhtml
            |-- signup.rhtml
            `-- welcome.rhtml


To create a generator we extend Rails::Generator::Base and define a manifest method which lists all the steps to take to generate the desired code. Here is the listing from basic_auth_generator.rb

class BasicAuthGenerator < Rails::Generator::Base
  def manifest
    record do |m|

      # Controller
      m.file "lib/basic_auth.rb", "lib/basic_auth.rb"
      m.file "controllers/user_controller.rb", "app/controllers/user_controller.rb" 

      # Models
      m.file "models/user.rb", "app/models/user.rb"
      m.file "models/notifications.rb", "app/models/notifications.rb"

      # Tests
      m.file "test/unit/user_test.rb", "test/unit/user_test.rb"
      m.file "test/functional/user_controller_test.rb", "test/functional/user_controller_test.rb"
      m.file "test/fixtures/users.yml", "test/fixtures/users.yml"

      # Views. 
      m.directory "app/views/notifications"
      m.directory "app/views/user"
      m.file "views/user/login.rhtml", "app/views/user/login.rhtml"
      m.file "views/user/signup.rhtml", "app/views/user/signup.rhtml"
      m.file "views/user/change_password.rhtml", "app/views/user/change_password.rhtml"
      m.file "views/user/forgot_password.rhtml", "app/views/user/forgot_password.rhtml"
      m.file "views/user/hidden.rhtml", "app/views/user/hidden.rhtml"
      m.file "views/user/hidden.rhtml", "app/views/user/welcome.rhtml"

      m.file "views/notifications/forgot_password.rhtml", "app/views/notifications/forgot_password.rhtml"


      m.migration_template "migrate/create_users.rb", "db/migrate"

      m.readme "INSTALL"
    end
  end

  def file_name
    "create_users"
  end

end

The basic_auth_generator is very simple. All it does it copy each file from the template directory to its correct destination. m.file copies the source file from the generator’s templates directory to the specified destination in the application that calls the generator. m.directory creates a new directory. m.migration_template creates a new migration. For this method we give it the source of the migration in the templates directory and a destination migration directory. It will choose an appropriate filename based on the migrations that already exist (m.migration uses the file_name method so we define it here to return the string “create_users”. We wouldn’t need to do this if we extended NamedBase - see below). m.readme prints the contents of a file. We use this to give the user further instructions after the generator is finished.

There is one change to the code from the previous authentication article. There are a couple of methods that we need to appear in our application controller. However we don’t want to overwrite the file application.rb as there may already be other code in it. We could print a message asking the user to paste the required code into application_controller.rb after generation. Instead we move these methods into their own module. We copy this module into the lib directory and print a message after generation asking the user to include them.

So the code destined for application.rb goes into lib/basic_auth.rb.

module BasicAuth

  def login_required
    ...
  end

  def current_user
    ...
  end

  def redirect_to_stored
    ...
  end  

end

and the user has to include this module in their application_controller.rb:

require_dependency "basic_auth"

class ApplicationController < ActionController::Base
  include BasicAuth
end

We print out an instruction to do this by putting it in the file INSTALL and calling m.readme "INSTALL" at the end of the manifest.

Going further

There is not much to this generator as it just uses the file method to copy files without modifying them. For more complex generators we can extend Rails::Generator::NamedBase instead of Rails::Generator::Base. This allows us to pass a name to the generator. We can use the template method to generate code based on the name passed to the generator. m.template passes the source file through erb and then copies the result to the destination file. Thus using the template method you can execute ruby code and use the results in your generated template.

As an example you might want to allow the user to specify the name of the controller to generate. If the generator is called using ruby script/generate Account, you can access the name using the methods class_name (returns “Account”) and file_name (returns “account”). You would then define your controller template using

class <%= class_name %>Controller < ApplicationController...

and in your manifest

m.template "controllers/user_controller.rb", "app/controllers/#{file_name}_controller.rb"

To learn how to do more complex things than just copying files have a look at the source for some of the available generators such as login_generator (run gem search -r generator to get a list of available generators) .

Where to put the generator

To use your generator across multiple applications you should put the generator directory somewhere that rails looks for generators such as ~/.rails/generators in your home directory or lib/generators in your application directory.

Using the generator

> ruby script/generate basic_auth
      create  lib/basic_auth.rb
      create  app/controllers/user_controller.rb
      create  app/models/user.rb
      create  app/models/notifications.rb
      create  test/unit/user_test.rb
      create  test/functional/user_controller_test.rb
      create  test/fixtures/users.yml
      create  app/views/notifications
      create  app/views/user
      create  app/views/user/login.rhtml
      create  app/views/user/signup.rhtml
      create  app/views/user/change_password.rhtml
      create  app/views/user/forgot_password.rhtml
      create  app/views/user/hidden.rhtml
      create  app/views/user/welcome.rhtml
      create  app/views/notifications/forgot_password.rhtml
      create  db/migrate
      create  db/migrate/001_create_users.rb
      readme  INSTALL

Download

You can download the generator here: basic_auth.tar.gz, basic_auth.zip

AttachmentSize
basic_auth.tar.gz7.11 KB
basic_auth.zip12.26 KB

This is, no doubt, a painfully newbie question, so I beg your indulgence if I'm missing something really obvious. I worked through your "basic user authentication in rails" manually, and everything worked beautifully.

I then wanted to try out this basic_auth generator, so I downloaded the tarball, and placed it within my RAILSAPP under lib/generators/ and ran "script/generate basic_auth".

I received the expected output, and it looks like all of the files were created properly and in the correct locations.

Following the installation directions, I then edited the application.rb to include the require_dependency and include lines as described.

Next, I ran "rake migrate". Instead of receiving feedback that my user table had been created, I received the following:

Mr-Gosh:~/Sites/RAILSAPP$ rake migrate --trace
(in /Users/steph/Sites/RAILSAPP)
** Invoke migrate (first_time)
** Invoke db:migrate (first_time)
** Invoke environment (first_time)
** Execute environment
** Execute db:migrate
rake aborted!
undefined method `migrate' for User:Class
...

So obviously I've screwed something up, but I can't figure out what's different in the basic_auth generated version of this, versus my manual attempt. If you have a moment, and an idea, any nudge or suggestion would be greatly appreciated ;) I have a pretty clear sense that "undefined method 'migrate'" should be telling me something fairly specific, but I just don't have the knowledge to grok it (nor is google turning up any clues.)

At any rate, thank you for your time, and a fantastic tutorial!

Looks like maybe the generated migration doesn't have the correct filename.

Try renaming the generated migration file to 001_create_users.rb (replace 001 with whatever migration number the existing migration file has).

I had to rename the file db/migrate/001_user.rb to db/migrate/001_create_user.rb and changed the name of the class to CreateUser (same file)

As all newrubies, I found your explanation very clear...
installation went as smoothly as predicted (excepted the same 'rake aborted' problem, solved as you stated... 001_users.rb is created, instead of 001_create_users.rb )..

running unit test OK
but running functional teste gave 1 failure :

1) Failure:
test_return_to(UserControllerTest) [./test/functional/user_controller_test.rb:167]:
response is not a redirection to all of the options supplied (redirection is <"http://localhost/user/hidden">), difference: <{:controller=>"user"}>

need more weeks of practice to understand what does it means ? or easy ?

thanks for yoru lights ..

yves

Nice follow-up to your tutorial, Aidan.

To avoid renaming migration file to xxx_create_users.rb you can use:


m.migration_template "migrate/create_users.rb", "db/migrate", :migration_file_name => 'create_users'

Then you can also remove def file_name "user" end at the end.

Hope it helps ;)

I have passed the user_test, but got an error on user_controller_test
=========
1) Failure:
test_return_to(UserControllerTest) [test/functional/user_controller_test.rb:167]:
response is not a redirection to all of the options supplied (redirection is <"http://localhost/user/hidden">), difference: <{:controller=>"user"}>
=========
I am very new in RoR, anybody can give a direction what is happening?

Tahnks a lot!!

the bug with the test in both of the above postings seems to be a bug in rails itself. (see submitted ticket).

To get the test to pass, change line 167 of user_controller_test.rb

from:
assert_redirected_to :action=>'hidden'

to:
assert_redirected_to @controller.url_for(:controller=>'user', :action=>'hidden')

Thanks, I noticed that test fails in some versions of rails but not others. Thanks for the fix.

Problem in customizing i have tried the generator and it is awesome. but whenever i customize the files, then generate it again and start the webrick server.... the page is loading and it throws an error in the log "missing default helper path", the page is totally empty.... looks like the server is not able to get the request....any idea....will help... Thanks in advance.

i´m trying to do a generator for some files that should have a distintive name...
how can i send a parameter to specifie that name, and create the file structure properly?

I upgraded to rails 2.0.2 and now my boiler plate generator doesn't work.

def manifest
record do |m|
m.directory "testing"
end
end

That does absolutely nothing. Neither does m.file. It worked fine in rails 1.2.

Any suggestions?

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
1 + 7 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.