Add new migrations from Rails engine gem to app via generator - ruby-on-rails

I'm building a Rails engine in a ruby gem. It includes some migrations right now that are called when you run:
rails g myengine:install
The code in the generator is as follows:
module MyEngine
module Generators
class InstallGenerator < ::Rails::Generators::Base
include Rails::Generators::Migration
source_root File.expand_path('../templates', __FILE__)
# ...
def copy_migrations
migration_template "migrations/migration1.rb", "db/migrate/migration1.rb"
migration_template "migrations/migration2.rb", "db/migrate/migration2.rb"
end
# ...
end
end
end
However, if I run rails g myengine:install again, it fails with this error:
Another migration is already named migration1: /Users/jh/Code/Web/demoapp/db/migrate/20130327222221_migration1.rb
I want it to just silently ignore the fact that there's already a migration and continue on to the next migration. What would be the best way to do this?
EDIT:
Per Dmitry's answer, this was my solution:
def copy_migrations
copy_migration "migration1"
copy_migration "migration2"
end
protected
def copy_migration(filename)
if self.class.migration_exists?("db/migrate", "#{filename}")
say_status("skipped", "Migration #{filename}.rb already exists")
else
migration_template "migrations/#{filename}.rb", "db/migrate/#{filename}.rb"
end
end

Using migration_template in Rails as example, you could perhaps check for destination = self.class.migration_exists?(migration_dir, #migration_file_name) and if migration already exists, skip over making migration_template call.

Related

How to access rails methods in a rails plugin

I followed this tutorial to create a rails plugin.
Section 3 explains how to add a method to string that is available anywhere in a Rails application. Now for my case, I want to be able to access rails methods in the plugin itself.
For example in lib/plugin/my_plugin/my_plugin.rb I want to use:
Rails.application.eager_load!
But every time running the gem with an executable it throws following error:
undefined method `eager_load!' for nil:NilClass (NoMethodError)
Now I know this error is thrown because there's no Rails application, but I added the plugin to a Rails application and it also doesn't seem to work (same error) and can't find another way to get this working. Am I approaching this problem the wrong way or is there even a better way?
lib/plugin/my_plugin/my_plugin.rb:
require 'rails'
class MyPlugin
def fetch_models
arr_models = []
Rails.application.eager_load!
::ApplicationRecord.descendants.each do |model|
arr_models[] << model
end
end
end
Steps reproduce:
Create a new plugin rails plugin new custom
Add lib/custom/generator.rb
lib/custom/generator.rb:
require 'faker'
require 'rails'
class Generator
def fetch_models
arr_models = []
Rails.application.eager_load!
ApplicationRecord.descendants.each do |model|
arr_models[] << model
end
end
end
Edit lib/custom.rb
lib/custom.rb:
require "custom/generator"
require "custom/version"
require "custom/railtie"
module Custom
def self.generate
generator = Generator.new
models = generator.fetch_models
end
end
Add executable in bin/
bin/custom:
#!/usr/bin/env ruby
require 'custom'
Custom.generate
Edit custom.gemspec so that bin/test passes
Execute following commands:
$ bundle exec rake build
$ bundle exec rake install
Plugins usually are built to extend core classes.
Using the generator and following the linked guide at my_plugin/lib/my_plugin/rails_core_ext.rb you have to use class you want to extend.
Your code become
class RailsPlayground::Application
def fetch_models
arr_models = []
Rails.application.eager_load!
ApplicationRecord.descendants.each do |model|
arr_models[] << model
end
end
end
or
class RailsPlayground::Application
def fetch_models
Rails.application.eager_load!
ApplicationRecord.descendants
end
end

Create a rails 5 plugin that need migrations

I want to create a rails 5+ gem that must create a migration as it would have to use a model and save/access data in DB.
My gem has some simple classes I stored in lib/ but it must use some that should access DB and this is what I don't know how to do.
Language should update itself in a DB
Languages should list all existing Languages and create a new Language
Basically in a rails app, I whould create the migrations and models, but I couldn't find tutorials about how to do that, and the rails doc did not help me.
Any help is appreciated.
Take a look at Rails Engines.
With an engine you can define generators, models, routes, controllers, etc and hook into a host rails app.
# routes
MyCustomGem::Engine.routes.draw do
resources :languages
end
# controllers
module MyCustomGem
class LanguagesController < ApplicationController
...
end
end
When installing this in a rails app, you'll use
$ bin/rails my_custom_gem:install:migrations
And that will create the migration in the host app.
If you don’t want to copy the migrate file, you can execute migrate directly in the application as follows
module MyEngine
class Engine < ::Rails::Engine
isolate_namespace MyEngine
initializer :append_migrations do |app|
unless app.root.to_s.match root.to_s
config.paths["db/migrate"].expanded.each do |expanded_path|
app.config.paths["db/migrate"] << expanded_path
end
end
end
end
end
When installing this in a rails app
bin/rails db:migrate

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.

Rails Gem: Running All Generators for given Namespace

I'm developing a gem core with multiple sub modules, each it's own gem. As a developer, you'll be able to install the core and any other of the gems. How can I create a rake task or generator to run the generators for ALL of the installed gems with generators under the main gem namespace.
Example, if I my gem is called admin:
module Admin
module Generators
class InstallGenerator < Rails::Generators::Base
end
end
end
And I have another generator for one of the sub-gems:
module Admin
module Generators
class PostsGenerator < Rails::Generators::Base
end
end
end
And another one:
module Admin
module Generators
class TagslGenerator < Rails::Generators::Base
end
end
end
And there might be up to 10 more gems that can be installed. Rather than rail g admin:... installing each one, I would like to create a rake task or generator that runs all of the tasks.
Thanks in advance!
Keep an "AllGenerator" class under Admin module. The generator will have to do the following :
For each class under the namespace that is a generator class,
get the namespace from classname.
Call the invoke method with the namespace.
Something like this :
module Admin
module Generators
class AllGenerator < Rails::Generators::Base
def generator
Rails::Generators.lookup!
Admin::Generators.constants.each do |const|
generator_class = Admin::Generators.const_get(const)
next if self.class == generator_class
if generator_class < Rails::Generators::Base
namespace = generator_klass_to_namespace(generator_class)
invoke(namespace)
end
end
end
private
def generator_klass_to_namespace(klass)
namespace = Thor::Util.namespace_from_thor_class(klass)
return namespace.sub(/_generator$/, '').sub(/:generators:/, ':')
end
end
end
end
Here's the link to the gist with complete tested code
This way, running rails g admin:all would run every other generator directly under Admin::Generators .
First check out the following question and answer.
Find classes available in a Module
So all you have to do is access
Admin::Generators.constants.each do |c|
c = Admin::Generators.const_get(c)
if c < Rails::Generators::Base
c.new.run(your_args)
end
end
Only thing is I have never invoked a generator like this so it might be a little bit more then c.new.run, but I think that should do it.

generators and migrations in plugins (rails 3)

I am simply trying to create a plugin migration generator without any parameters, like : $rails generate yaffle and this should copy the migration file (lib/generators/yaffle/template/create_yaffle.rb) to db/migrate/[timestamp]_create_yaffle.rb.
The problem I am facing here is, its copying, but without timestamp.
Also, when I run $rails generate yaffle it gives me a message that arguments are not provided, it expects to be in this format rails generate yaffle NAME [options]. I dont want to have any options/arguments, it should just be rails generate yaffle.
What should I do?
I followed the generator used in acts_as_commentable , it looks pretty simple, but I don't know where to modify these settings... can anybody help?
Generator Code:
require 'rails/generators'
require 'rails/generators/migration'
class ThumbitGenerator Rails::Generators::NamedBase
source_root File.expand_path('../templates', __FILE__)
def self.next_migration_number(path)
Time.now.utc.strftime("%Y%m%d%H%M%S")
end
def create_model_file
template "like.rb", "app/models/like.rb"
template "liking.rb", "app/models/liking.rb"
template "create_likes.rb", "db/migrate/create_likes.rb"
template "create_likings.rb", "db/migrate/create_likings.rb"
end
end
Ok, I found the answer...
I was using Rails::Generators::NamedBase instead of Rails::Generators::Base in my generator file! When you use NamedBase, it always expects an argument to be passed (which is the name of initializer) Explanation : guides.rubyonrails.org/generators
And I was using template method instead of migration_template because of which migration files din't produce any migration number Explanation: Rails::Generators::Migration.migration_template
So finally, this worked!
require 'rails/generators'
require 'rails/generators/migration'
class ThumbitGenerator < Rails::Generators::Base
include Rails::Generators::Migration
source_root File.expand_path('../templates', __FILE__)
def self.next_migration_number(path)
Time.now.utc.strftime("%Y%m%d%H%M%S")
end
def create_model_file
template "like.rb", "app/models/like.rb"
template "liking.rb", "app/models/liking.rb"
migration_template "create_likes.rb", "db/migrate/create_likes.rb"
migration_template "create_likings.rb", "db/migrate/create_likings.rb"
end
end
A small polish on the solution - to save yourself the hassle of defining the timestamp for the migration and future proof your generator in case Rails core team decides to use another way of stamping (e.g. SHA hashes truncated to 10 characters), you can require 'rails/generators/active_record' and extend ActiveRecord::Generators::Migration like this:
require 'rails/generators'
require 'rails/generators/migration'
require 'rails/generators/active_record'
class ThumbitGenerator < Rails::Generators::Base
include Rails::Generators::Migration
extend ActiveRecord::Generators::Migration
source_root File.expand_path('../templates', __FILE__)
def create_model_file
template "like.rb", "app/models/like.rb"
template "liking.rb", "app/models/liking.rb"
migration_template "create_likes.rb", "db/migrate/create_likes.rb"
migration_template "create_likings.rb", "db/migrate/create_likings.rb"
end
end
UPDATE In Rails 4 ActiveRecord::Generators::Migration is no longer a module, so use instead:
require 'rails/generators'
require 'rails/generators/migration'
require 'rails/generators/active_record'
class ThumbitGenerator < Rails::Generators::Base
include Rails::Generators::Migration
# Implement the required interface for Rails::Generators::Migration
def self.next_migration_number(dirname)
ActiveRecord::Generators::Base.next_migration_number(dirname)
end
source_root File.expand_path('../templates', __FILE__)
def create_model_file
template "like.rb", "app/models/like.rb"
template "liking.rb", "app/models/liking.rb"
migration_template "create_likes.rb", "db/migrate/create_likes.rb"
migration_template "create_likings.rb", "db/migrate/create_likings.rb"
end
end
you can simply inherit from ActiveRecord::Generators::Base and everything will work

Resources