Rails: how to extend a generator? - ruby-on-rails

I'm trying to extend the model generator in Rails ( rails g model ). Basically my generator should do the same thing as the model one, but copy 2 additional files. Simple as that.
I reviewed Railscast #218 ( http://railscasts.com/episodes/218-making-generators-in-rails-3 ) which was very informative but i couldn't find any info about extending generators.
Checking the source code of rails, it looks like the model generator is in lib/rails/generators/rails/model/model_generator.rb defined as Rails::Generators::ModelGenerator.
I tried to make my generator extend this class but it results in:
Error: uninitialized constant Rails::Generators::ModelGenerator.
And my attempts to require this file were not successful.
So I decided to stop and ask here. What is the proper way of extending a generator?

Take a look at hooks and invoke.
class MyGenerator < Rails::Generators::Base
def create_my_file
# Do your generators stuff
create_file "config/initializers/my.rb", "# Add content here"
# Create model
invoke("model", ["model_name", "arg1", "arg2"])
end
end
Hope this help.

Generate your custom generator:
rails generate generator my_model
Open lib/generators/my_model/my_model_generator.rb and change it to:
require 'rails/generators/active_record/model/model_generator'
class MyModelGenerator < ActiveRecord::Generators::ModelGenerator
source_root File.expand_path('../templates', __FILE__)
end
This works for rails engines. Don't forget to add required templates.

Related

How Do I Create A Custom Rails Generate Model function?

I have a Rails 5 application that has separate migrations for three databases. I'm adding a new database. I normally do a rails g model field1:type1 field2:type2 field3:type.... to create my database tables with the desired fields. I want to create generate model statements where it will generate the model for the desired database and put it in the corresponding db/migrate folder.
I have custom database migration generators for the additional databases in lib/generators. Here is an example of the custom migration generator.
lib/generators/stats_migration_generator.rb
require 'rails/generators/active_record/migration/migration_generator'
class StatsMigrationGenerator < ActiveRecord::Generators::MigrationGenerator
source_root File.join(File.dirname(ActiveRecord::Generators::MigrationGenerator.instance_method(:create_migration_file).source_location.first), "templates")
def create_migration_file
set_local_assigns!
validate_file_name!
migration_template #migration_template, "db_stats/migrate/#{file_name}.rb"
end
end
I assume I need to create lib/generators/mydb_model_generator.rb or whatever the correct filename structure is for each additional database. I'm thinking I could do something like this after looking at the model_generator.rb file in GitHub.
lib/generators/stats_model_generator.rb
require 'rails/generators/active_record/model/model_generator'
class StatsModelGenerator < ActiveRecord::Generators::ModelGenerator
source_root File.join(File.dirname(ActiveRecord::Generators::ModelGenerator.instance_method(:create_migration_file).source_location.first), "templates")
def create_migration_file
set_local_assigns!
validate_file_name!
migration_template #migration_template, "db_stats/migrate/#{file_name}.rb"
end
end
Here are my questions after looking at the git repository for Rails, the Rails Guide regarding creating and customizing Generators and the Ruby on Rails API documentation:
How do I find what I need to require?
How do I find what method I need to override the db/migrate folder name?
How do I find out how to name the rb file for the model generator?
Are my assumptions correct in my attempt to create the model generator?
Well I did not think I would find the solution so quickly. I took another look at model_generator.rb in GitHub and had an aha moment. I realized that I just needed to replace the method in the default generator with my version which points to the db migrate folder I want. Here is the solution I came up with.
require 'rails/generators/active_record/model/model_generator'
class MydbModelGenerator < ActiveRecord::Generators::ModelGenerator
source_root File.join(File.dirname(ActiveRecord::Generators::ModelGenerator.instance_method(:create_migration_file).source_location.first), "templates")
def create_migration_file
return unless options[:migration] && options[:parent].nil?
attributes.each { |a| a.attr_options.delete(:index) if a.reference? && !a.has_index? } if options[:indexes] == false
migration_template "../../migration/templates/create_table_migration.rb", File.join("db_mydb/migrate", "create_#{table_name}.rb")
end
end
Now I can do rails g mydb_model field1:type1 field2:type2 field3:type and it creates the model migration file in the correct migration folder.

Nested modules in Rails breaking table_name_prefix

I'm trying to use nested module/class definitions consistently in a Rails app, rather than the compact (::) syntax. However, it doesn't always load the module file itself, which contains the table_name_prefix.
Using Rails 4.1.8 on Ruby 2.1.1...
rails new my_app
...
rails g scaffold User
rails g scaffold Blog::Post
This creates app/models/blog.rb:
module Blog
def self.table_name_prefix
'blog_'
end
end
There seem to be many ways of accidentally preventing Rails from auto-loading blog.rb. The simplest example is via the helpers.
Change app/helpers/blog/posts_helper.rb from:
module Blog::PostsHelper
end
to:
module Blog
module PostsHelper
end
end
Launch the server, visit /users and then visit /blog/posts:
SQLite3::SQLException: no such table: posts: SELECT "posts".* FROM "posts"
Similar problems can occur elsewhere, such as in the model tests. It's not limited to the helpers.
What's the best way of resolving this? Explicitly loading blog.rb and any other namespace modules?
One solution, which doesn't rely on autoloading, is to set the models to inherit from the following, instead of from ActiveRecord::Base directly:
class CustomActiveRecordBase < ActiveRecord::Base
self.abstract_class = true
# If no table name prefix has been defined, include the namespace/module as
# table name prefix, e.g., Blog:: -> blog_
def self.table_name
# If a table_name_prefix has been defined, follow default behaviour
return super if full_table_name_prefix.present?
# Find the prefix, e.g., Blog::Post -> 'blog', User -> ''
prefix = model_name.name.deconstantize.underscore
# If no prefix, follow default behaviour
return super unless prefix.present?
# Otherwise add the prefix with an underscore
"#{prefix}_#{super}"
end
end
Then there is no need to define self.table_name_prefix in blog.rb.
These can be set as defaults for future models through appropriate files in lib/templates/active_record/model/model.rb and module.rb, based on the default active record templates.
This could all be done by monkey-patching ActiveRecord::Base, but this interferes with other classes, such as ActiveRecord::SchemaMigration, which doesn't have a table prefix.

Add custom generator in addition to default rails generators

What I need is to add some translations to i18n on scaffold generating. I'd like to know: is there possibility to force rails g scaffold to invoke my own generator in addition to defaults?
If not, how can I invoke default Rails generators (e.g. active_record) in my generator?
Thank you!
According to the doc here, you can use generate from your generator to invoke another generator.
generate a_generator_name, args_as_string
class FooGenerator < Rails::Generators::Base
argument :attributes, :type => :array
def call
do_your_stuff
run_scaffold
end
private
def do_your_stuff
end
def run_scaffold
generate 'scaffold', attributes.join(' ')
end
end

Rails: hook_for :orm not finding active_record

I'm writing a custom generator. For the most part, the generator is able to use hooks successfully. For instance,
hook_for :resource_route, in: :rails, required: true
Invokes resource_route as expected. However:
hook_for :orm, in: :rails, required: true
Returns the error:
error active_record [not found]
I'm assuming this is because the active_record_generator is located in a dramatically different directory from other generators, such as the resource_route generator.
rails / activerecord / lib / rails / generators / active_record.rb
rails / railties / lib / rails / generators / rails / resource_route /
resource_route_generator.rb
Is there a way to get my generator to properly hook active record?
I eventually hacked this, only after I managed a dodgy work around using hook_for and remove_hook_for fiasco. My advise leave it alone it's not worth the trouble.
This way uses much less code for way more result for your effort.
There are a few tricks you may want to use.
Stay within the Rails::Generators namespace.
The folder structure I used was:
lib/generators/my_own_model/
templates/
my_own_model_generator.rb
Set the config in the model for the generator you want to create
That's all you need to overcome the error whatever [not found] headaches.
Code looks like this
require 'rails/generators/active_record/model/model_generator'
module Rails
module Generators
hide_namespace 'my_own_model'
class Railtie < ::Rails::Engine
if config.respond_to?(:app_generators)
config.app_generators.orm = :my_own_model
else
config.generators.orm = :my_own_model
end
end
class MyOwnModelGenerator < ActiveRecord::Generators::ModelGenerator
source_root "#{base_root}/active_record/model/templates"
# all public methods will get executed by rails g
protected
# these won't but can be overwritten by sub classes
private
# these are still exposed to your templates nifty
end
end
end
The third trick is to hide your generator name. Why is that you ask? Well you may like to know that you won't be needing it anymore.
$ rails g model MyNewModel generator:belongs_to my_own:boolean
You have just created a default generator which you can overwrite again from your template folders. =)
nJoy!
In Rails 5 at least, you need to specify the type of generator (:model, :migration, :application_record)
hook_for :orm, as: :model

Creating a rails 3 generator

I'm creating a rails generator:
class TaggableGenerator < Rails::Generators::NamedBase
source_root File.expand_path('../templates', __FILE__)
hook_for :orm, :as => "model"
end
Everything works fine, but I would like to set the fields created in the models and create multiple models, I can't find anything about how to do it (I got the above code from looking at the devise generators) preferably I'd like it to me orm generic (but its not that important).
Here is some links that may help you:
http://railscasts.com/episodes/218-making-generators-in-rails-3
http://guides.rubyonrails.org/generators.html
Basically you just need to add methods in your class to do things you want (all the public methods will get called when the generator is called), here is an example from rails sources:
class AssetsGenerator < Rails::Generators::NamedBase
source_root File.expand_path("../templates", __FILE__)
def copy_stylesheet
copy_file "stylesheet.css", File.join('app/assets/stylesheets', class_path, "#{file_name}.css")
end
end
the copy_file comes from Thor, you wan see a list of availables methods in the Thor reference: http://textmate.rubyforge.org/thor/Thor/Actions.html

Resources