Creating and Customizing Rails Generators & Templates
Rails generators and application templates are useful tools that can help improve your workflow by automatically creating boilerplate code. In this guide you will learn:
- How to see which generators are available in your application.
- How to create a generator using templates.
- How Rails searches for generators before invoking them.
- How to customize Rails scaffolding by overriding generators and templates.
- How to use fallbacks to avoid overwriting a huge set of generators.
- How to use templates to create/customize Rails applications.
- How to use the Rails Template API to write your own reusable application templates.
1. First Contact
When you create an application using therails command, you are in fact usinga Rails generator. After that, you can get a list of all available generators byinvokingbin/rails generate:
$railsnew myapp$cdmyapp$bin/railsgenerateTo create a Rails application we use therails global command which usesthe version of Rails installed viagem install rails. When inside thedirectory of your application, we use thebin/rails command which uses theversion of Rails bundled with the application.
You will get a list of all generators that come with Rails. To see a detaileddescription of a particular generator, invoke the generator with the--helpoption. For example:
$bin/railsgenerate scaffold--help2. Creating Your First Generator
Generators are built on top ofThor, whichprovides powerful options for parsing and a great API for manipulating files.
Let's build a generator that creates an initializer file namedinitializer.rbinsideconfig/initializers. The first step is to create a file atlib/generators/initializer_generator.rb with the following content:
classInitializerGenerator<Rails::Generators::Basedefcreate_initializer_filecreate_file"config/initializers/initializer.rb",<<~RUBY # Add initialization content here RUBYendendOur new generator is quite simple: it inherits fromRails::Generators::Baseand has one method definition. When a generator is invoked, each public methodin the generator is executed sequentially in the order that it is defined. Ourmethod invokescreate_file, which will create a file at the givendestination with the given content.
To invoke our new generator, we run:
$bin/railsgenerate initializerBefore we go on, let's see the description of our new generator:
$bin/railsgenerate initializer--helpRails is usually able to derive a good description if a generator is namespaced,such asActiveRecord::Generators::ModelGenerator, but not in this case. We cansolve this problem in two ways. The first way to add a description is by callingdesc inside our generator:
classInitializerGenerator<Rails::Generators::Basedesc"This generator creates an initializer file at config/initializers"defcreate_initializer_filecreate_file"config/initializers/initializer.rb",<<~RUBY # Add initialization content here RUBYendendNow we can see the new description by invoking--help on the new generator.
The second way to add a description is by creating a file namedUSAGE in thesame directory as our generator. We are going to do that in the next step.
3. Creating Generators with Generators
Generators themselves have a generator. Let's remove ourInitializerGeneratorand usebin/rails generate generator to generate a new one:
$rmlib/generators/initializer_generator.rb$bin/railsgenerate generator initializer create lib/generators/initializer create lib/generators/initializer/initializer_generator.rb create lib/generators/initializer/USAGE create lib/generators/initializer/templates invoke test_unit create test/lib/generators/initializer_generator_test.rbThis is the generator just created:
classInitializerGenerator<Rails::Generators::NamedBasesource_rootFile.expand_path("templates",__dir__)endFirst, notice that the generator inherits fromRails::Generators::NamedBaseinstead ofRails::Generators::Base. This means that our generator expects atleast one argument, which will be the name of the initializer and will beavailable to our code vianame.
We can see that by checking the description of the new generator:
$bin/railsgenerate initializer--helpUsage: bin/rails generate initializer NAME [options]Also, notice that the generator has a class method calledsource_root.This method points to the location of our templates, if any. By default itpoints to thelib/generators/initializer/templates directory that was justcreated.
In order to understand how generator templates work, let's create the filelib/generators/initializer/templates/initializer.rb with the followingcontent:
# Add initialization content hereAnd let's change the generator to copy this template when invoked:
classInitializerGenerator<Rails::Generators::NamedBasesource_rootFile.expand_path("templates",__dir__)defcopy_initializer_filecopy_file"initializer.rb","config/initializers/#{file_name}.rb"endendNow let's run our generator:
$bin/railsgenerate initializer core_extensions create config/initializers/core_extensions.rb$catconfig/initializers/core_extensions.rb# Add initialization content hereWe see thatcopy_file createdconfig/initializers/core_extensions.rbwith the contents of our template. (Thefile_name method used in thedestination path is inherited fromRails::Generators::NamedBase.)
4. Generator Command Line Options
Generators can support command line options usingclass_option. Forexample:
classInitializerGenerator<Rails::Generators::NamedBaseclass_option:scope,type: :string,default:"app"endNow our generator can be invoked with a--scope option:
$bin/railsgenerate initializer theme--scope dashboardOption values are accessible in generator methods viaoptions:
defcopy_initializer_file@scope=options["scope"]end5. Generator Resolution
When resolving a generator's name, Rails looks for the generator using multiplefile names. For example, when you runbin/rails generate initializer core_extensions,Rails tries to load each of the following files, in order, until one is found:
rails/generators/initializer/initializer_generator.rbgenerators/initializer/initializer_generator.rbrails/generators/initializer_generator.rbgenerators/initializer_generator.rb
If none of these are found, an error will be raised.
We put our generator in the application'slib/ directory because thatdirectory is in$LOAD_PATH, thus allowing Rails to find and load the file.
6. Overriding Rails Generator Templates
Rails will also look in multiple places when resolving generator template files.One of those places is the application'slib/templates/ directory. Thisbehavior allows us to override the templates used by Rails' built-in generators.For example, we could override thescaffold controller template or thescaffold view templates.
To see this in action, let's create alib/templates/erb/scaffold/index.html.erb.ttfile with the following contents:
<%%=@<%= plural_table_name%>.count %><%= human_name.pluralize%>Note that the template is an ERB template that rendersanother ERB template.So any<% that should appear in theresulting template must be escaped as<%% in thegenerator template.
Now let's run Rails' built-in scaffold generator:
$bin/railsgenerate scaffold Post title:string ... create app/views/posts/index.html.erb ...The contents ofapp/views/posts/index.html.erb is:
<%=@posts.count%> Posts7. Overriding Rails Generators
Rails' built-in generators can be configured viaconfig.generators,including overriding some generators entirely.
First, let's take a closer look at how the scaffold generator works.
$bin/railsgenerate scaffold User name:string invoke active_record create db/migrate/20230518000000_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml invoke resource_route route resources :users invoke scaffold_controller create app/controllers/users_controller.rb invoke erb create app/views/users create app/views/users/index.html.erb create app/views/users/edit.html.erb create app/views/users/show.html.erb create app/views/users/new.html.erb create app/views/users/_form.html.erb create app/views/users/_user.html.erb invoke resource_route invoke test_unit create test/controllers/users_controller_test.rb create test/system/users_test.rb invoke helper create app/helpers/users_helper.rb invoke test_unit invoke jbuilder create app/views/users/index.json.jbuilder create app/views/users/show.json.jbuilderFrom the output, we can see that the scaffold generator invokes othergenerators, such as thescaffold_controller generator. And some of thosegenerators invoke other generators too. In particular, thescaffold_controllergenerator invokes several other generators, including thehelper generator.
Let's override the built-inhelper generator with a new generator. We'll namethe generatormy_helper:
$bin/railsgenerate generatorrails/my_helper create lib/generators/rails/my_helper create lib/generators/rails/my_helper/my_helper_generator.rb create lib/generators/rails/my_helper/USAGE create lib/generators/rails/my_helper/templates invoke test_unit create test/lib/generators/rails/my_helper_generator_test.rbAnd inlib/generators/rails/my_helper/my_helper_generator.rb we'll definethe generator as:
classRails::MyHelperGenerator<Rails::Generators::NamedBasedefcreate_helper_filecreate_file"app/helpers/#{file_name}_helper.rb",<<~RUBY module#{class_name}Helper # I'm helping! end RUBYendendFinally, we need to tell Rails to use themy_helper generator instead of thebuilt-inhelper generator. For that we useconfig.generators. Inconfig/application.rb, let's add:
config.generatorsdo|g|g.helper:my_helperendNow if we run the scaffold generator again, we see themy_helper generator inaction:
$bin/railsgenerate scaffold Article body:text ... invoke scaffold_controller ... invoke my_helper create app/helpers/articles_helper.rb ...You may notice that the output for the built-inhelper generatorincludes "invoke test_unit", whereas the output formy_helper does not.Although thehelper generator does not generate tests by default, it doesprovide a hook to do so usinghook_for. We can do the same by includinghook_for :test_framework, as: :helper in theMyHelperGenerator class. Seethehook_for documentation for more information.
7.1. Generators Fallbacks
Another way to override specific generators is by usingfallbacks. A fallbackallows a generator namespace to delegate to another generator namespace.
For example, let's say we want to override thetest_unit:model generator withour ownmy_test_unit:model generator, but we don't want to replace all of theothertest_unit:* generators such astest_unit:controller.
First, we create themy_test_unit:model generator inlib/generators/my_test_unit/model/model_generator.rb:
moduleMyTestUnitclassModelGenerator<Rails::Generators::NamedBasesource_rootFile.expand_path("templates",__dir__)defdo_different_stuffsay"Doing different stuff..."endendendNext, we useconfig.generators to configure thetest_framework generator asmy_test_unit, but we also configure a fallback such that any missingmy_test_unit:* generators resolve totest_unit:*:
config.generatorsdo|g|g.test_framework:my_test_unit,fixture:falseg.fallbacks[:my_test_unit]=:test_unitendNow when we run the scaffold generator, we see thatmy_test_unit has replacedtest_unit, but only the model tests have been affected:
$bin/railsgenerate scaffold Comment body:text invoke active_record create db/migrate/20230518000000_create_comments.rb create app/models/comment.rb invoke my_test_unit Doing different stuff... invoke resource_route route resources :comments invoke scaffold_controller create app/controllers/comments_controller.rb invoke erb create app/views/comments create app/views/comments/index.html.erb create app/views/comments/edit.html.erb create app/views/comments/show.html.erb create app/views/comments/new.html.erb create app/views/comments/_form.html.erb create app/views/comments/_comment.html.erb invoke resource_route invoke my_test_unit create test/controllers/comments_controller_test.rb create test/system/comments_test.rb invoke helper create app/helpers/comments_helper.rb invoke my_test_unit invoke jbuilder create app/views/comments/index.json.jbuilder create app/views/comments/show.json.jbuilder8. Application Templates
Application templates are a little different from generators. While generatorsadd files to an existing Rails application (models, views, etc.), templates areused to automate the setup of a new Rails application. Templates are Rubyscripts (typically namedtemplate.rb) that customize new Rails applicationsright after they are generated.
Let's see how to use a template while creating a new Rails application.
8.1. Creating and Using Templates
Let's start with a sample template Ruby script. The below template adds Deviseto theGemfile after asking the user and also allows the user to name theDevise user model. Afterbundle install has been run, the template runs theDevise generators and also runs migrations. Finally, the template doesgit add andgit commit.
# template.rbifyes?("Would you like to install Devise?")gem"devise"devise_model=ask("What would you like the user model to be called?",default:"User")endafter_bundledoifdevise_modelgenerate"devise:install"generate"devise",devise_modelrails_command"db:migrate"endgitadd:".",commit:%(-m 'Initial commit')endTo apply this template while creating a new Rails application, you need toprovide the location of the template using the-m option:
$railsnew blog-m ~/template.rbThe above will create a new Rails application calledblog that has Devise gem configured.
You can also apply templates to an existing Rails application by usingapp:template command. The location of the template needs to be passed in viatheLOCATION environment variable:
$bin/railsapp:templateLOCATION=~/template.rbTemplates don't have to be stored locally, you can also specify a URL insteadof a path:
$railsnew blog-m https://example.com/template.rb$bin/railsapp:templateLOCATION=https://example.com/template.rbCaution should be taken when executing remote scripts from third parties. Since the template is a plain Ruby script, it can easily contain code that compromises your local machine (such as download a virus, delete files or upload your private files to a server).
The abovetemplate.rb file uses helper methods such asafter_bundle andrails_command and also adds user interactivity with methods likeyes?. Allof these methods are part of theRails TemplateAPI. Thefollowing sections shows how to use more of these methods with examples.
9. Rails Generators API
Generators and the template Ruby scripts have access to several helper methodsusing aDSL (DomainSpecific Language). These methods are part of the Rails Generators API and youcan find more details atThor::Actions andRails::Generators::Actions API documentation.
Here's another example of a typical Rails template that scaffolds a model, runsmigrations, and commits the changes with git:
# template.rbgenerate(:scaffold,"person name:string")route"root to: 'people#index'"rails_command("db:migrate")after_bundledogit:initgitadd:"."gitcommit:%Q{ -m 'Initial commit' }endAll code snippets in the examples below can be used in a templatefile, such as thetemplate.rb file above.
9.1. add_source
Theadd_source method adds the given source to the generated application'sGemfile.
add_source"https://rubygems.org"If a block is given, gem entries in the block are wrapped into the source group.For example, if you need to source a gem from"http://gems.github.com":
add_source"http://gems.github.com/"dogem"rspec-rails"end9.2. after_bundle
Theafter_bundle method registers a callback to be executed after the gemsare bundled. For example, it would make sense to run the "install" command fortailwindcss-rails anddevise only after those gems are bundled:
# Install gemsafter_bundledo# Install TailwindCSSrails_command"tailwindcss:install"# Install Devisegenerate"devise:install"endThe callbacks get executed even if--skip-bundle has been passed.
9.3. environment
Theenvironment method adds a line inside theApplication class forconfig/application.rb. Ifoptions[:env] is specified, the line is appendedto the corresponding file inconfig/environments.
environment'config.action_mailer.default_url_options = {host: "http://yourwebsite.example.com"}',env:"production"The above will add the config line toconfig/environments/production.rb.
9.4. gem
Thegem helper adds an entry for the given gem to the generated application'sGemfile.
For example, if your application depends on the gemsdevise andtailwindcss-rails:
gem"devise"gem"tailwindcss-rails"Note that this method only adds the gem to theGemfile, it does not installthe gem.
You can also specify an exact version:
gem"devise","~> 4.9.4"And you can also add comments that will be added to theGemfile:
gem"devise",comment:"Add devise for authentication."9.5. gem_group
Thegem_group helper wraps gem entries inside a group. For example, to loadrspec-railsonly in thedevelopment andtest groups:
gem_group:development,:testdogem"rspec-rails"end9.6. generate
You can even call a generator from inside atemplate.rb with thegenerate method. The following runs thescaffold rails generator withthe given arguments:
generate(:scaffold,"person","name:string","address:text","age:number")9.7. git
Rails templates let you run any git command with thegit helper:
git:initgitadd:"."gitcommit:"-a -m 'Initial commit'"9.8. initializer, vendor, lib, file
Theinitializer helper method adds an initializer to the generatedapplication'sconfig/initializers directory.
After adding the below to thetemplate.rb file, you can useObject#not_nil?andObject#not_blank? in your application:
initializer"not_methods.rb",<<-CODE class Object def not_nil? !nil? end def not_blank? !blank? end endCODESimilarly, thelib method creates a file in thelib/ directory andvendor method creates a file in thevendor/ directory.
There is also afile method (which is an alias forcreate_file), whichaccepts a relative path fromRails.root and creates all the directories andfiles needed:
file"app/components/foo.rb",<<-CODE class Foo endCODEThe above will create theapp/components directory and putfoo.rb in there.
9.9. rakefile
Therakefile method creates a new Rake file underlib/tasks with thegiven tasks:
rakefile("bootstrap.rake")do<<-TASK namespace :boot do task :strap do puts "I like boots!" end end TASKendThe above createslib/tasks/bootstrap.rake with aboot:strap rake task.
9.10. run
Therun method executes an arbitrary command. Let's say you want to removetheREADME.rdoc file:
run"rm README.rdoc"9.11. rails_command
You can run the Rails commands in the generated application with therails_command helper. Let's say you want to migrate the database at somepoint in the template ruby script:
rails_command"db:migrate"Commands can be run with a different Rails environment:
rails_command"db:migrate",env:"production"You can also run commands that should abort application generation if they fail:
rails_command"db:migrate",abort_on_failure:true9.12. route
Theroute method adds an entry to theconfig/routes.rb file. To makePeopleController#index the default page for the application, we can add:
route"root to: 'person#index'"There are also many helper methods that can manipulate the local file system,such ascopy_file,create_file,insert_into_file, andinside. You can see theThor APIdocumentation for details.Here is an example of one such method:
9.13. inside
Thisinside method enables you to run a command from a given directory.For example, if you have a copy of edge rails that you wish to symlink from yournew apps, you can do this:
inside("vendor")dorun"ln -s ~/my-forks/rails rails"endThere are also methods that allow you to interact with the user from the Ruby template, such asask,yes, andno. You can learn about all user interactivity methods in theThor Shell documentation. Let's see examples of usingask,yes? andno?:
9.14. ask
Theask methods allows you to get feedback from the user and use it in yourtemplates. Let's say you want your user to name the new shiny library you'readding:
lib_name=ask("What do you want to call the shiny library?")lib_name<<".rb"unlesslib_name.index(".rb")liblib_name,<<-CODE class Shiny endCODE9.15. yes? or no?
These methods let you ask questions from templates and decide the flow based onthe user's answer. Let's say you want to prompt the user to run migrations:
rails_command("db:migrate")ifyes?("Run database migrations?")# no? questions acts the opposite of yes?10. Testing Generators
Rails provides testing helper methods viaRails::Generators::Testing::Behavior, such as:
If running tests against generators you will need to setRAILS_LOG_TO_STDOUT=true in order for debugging tools to work.
RAILS_LOG_TO_STDOUT=true ./bin/testtest/generators/actions_test.rbIn addition to those, Rails also provides additional assertions viaRails::Generators::Testing::Assertions.