Creating and Customizing Rails Generators & Templates
Rails generators are an essential tool for improving your workflow. With thisguide you will learn how to create generators and customize existing ones.
After reading this guide, you will know:
- 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 your scaffold by overriding generator templates.
- How to customize your scaffold by overriding generators.
- How to use fallbacks to avoid overwriting a huge set of generators.
- How to create an application template.
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/railsgenerate
To 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--help
option. For example:
$bin/railsgenerate scaffold--help
2. 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.rb
insideconfig/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 RUBYendend
Our new generator is quite simple: it inherits fromRails::Generators::Base
and 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 initializer
Before we go on, let's see the description of our new generator:
$bin/railsgenerate initializer--help
Rails 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 RUBYendend
Now 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 ourInitializerGenerator
and 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.rb
This is the generator just created:
classInitializerGenerator<Rails::Generators::NamedBasesource_rootFile.expand_path("templates",__dir__)end
First, notice that the generator inherits fromRails::Generators::NamedBase
instead 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 here
And 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"endend
Now let's run our generator:
$bin/railsgenerate initializer core_extensions create config/initializers/core_extensions.rb$catconfig/initializers/core_extensions.rb#Add initialization content here
We see thatcopy_file
createdconfig/initializers/core_extensions.rb
with 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"end
Now our generator can be invoked with a--scope
option:
$bin/railsgenerate initializer theme--scope dashboard
Option values are accessible in generator methods viaoptions
:
defcopy_initializer_file@scope=options["scope"]end
5. 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.rb
generators/initializer/initializer_generator.rb
rails/generators/initializer_generator.rb
generators/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.tt
file 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%> Posts
7. 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.jbuilder
From 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_controller
generator 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.rb
And 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 RUBYendend
Finally, 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_helperend
Now 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..."endendend
Next, 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_unitend
Now 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.jbuilder
8. Application Templates
Application templates are a special kind of generator. They can use all of thegenerator helper methods, but are written as a Rubyscript instead of a Ruby class. Here is an example:
# 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')end
First, the template asks the user whether they would like to install Devise.If the user replies "yes" (or "y"), the template adds Devise to theGemfile
,and asks the user for the name of the Devise user model (defaulting toUser
).Later, afterbundle install
has been run, the template will run the Devisegenerators andrails db:migrate
if a Devise model was specified. Finally, thetemplate willgit add
andgit commit
the entire app directory.
We can run our template when generating a new Rails application by passing the-m
option to therails new
command:
$railsnew my_cool_app-m path/to/template.rb
Alternatively, we can run our template inside an existing application withbin/rails app:template
:
$bin/railsapp:templateLOCATION=path/to/template.rb
Templates also don't need to be stored locally — you can specify a URL insteadof a path:
$railsnew my_cool_app-m http://example.com/template.rb$bin/railsapp:templateLOCATION=http://example.com/template.rb
9. Generator Helper Methods
Thor provides many generator helper methods viaThor::Actions
, such as:
In addition to those, Rails also provides many helper methods viaRails::Generators::Actions
, such as:
10. Testing Generators
Rails provides testing helper methods viaRails::Generators::Testing::Behaviour
, 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.rb
In addition to those, Rails also provides additional assertions viaRails::Generators::Testing::Assertions
.
Back to top
[8]ページ先頭