Friday, December 14, 2007

Generator replacement for plugin_migration

I struggled last Friday trying to make loaded_plugins and plugin_migrations work in Rails 2.0. I was deep in the Rails source code when Seth suggest I simply create a generator. This is pretty easy. Here's what I came up with:


class MyPluginGenerator < Rails::Generator::NamedBase

def initialize(runtime_args, runtime_options = {})
super
@argument = runtime_args.shift
end

def manifest
relative_migration_path = "../../../db/migrate/"
recorded_session = record do |m|
get_migrations.each do | mf |
new_migration_file_name = mf.sub(/^(\d{3})_/,'')
new_migration_file_name.sub!(/\.rb$/,'')
unless migration_exists(new_migration_file_name)
m.migration_template "#{relative_migration_path}/#{mf}", 'db/migrate',
:assigns => { :migration_name => new_migration_file_name.camelize},
:migration_file_name => new_migration_file_name
puts " CREATING #{new_migration_file_name}."
else
puts " exists #{new_migration_file_name}, skipping."
end
end
end
end

protected
# Override with your own usage banner.
def banner
"Usage: #{$0} my_plugin migration"
end

def migration_exists(file_name)
migration_directory = File.dirname(__FILE__) + "/../../../../../db/migrate"
not Dir.glob("#{migration_directory}/[0-9]*_*.rb").grep(/[0-9]+_#{file_name}.rb$/).blank?
end

def get_migrations
migration_files = []
my_plugin_migration_path = File.dirname(__FILE__) + '/../../db/migrate'
Dir.entries(my_plugin_migration_path).sort.each do | migration_file |
if migration_file =~ /^(\d{3})/
migration_files << migration_file
end
end
migration_files
end


end

I place this in the file:
vendor/plugins/my_plugin/generators/my_plugin/my_plugin_generator.rb

To run it, I do:

ruby script/generate my_plugin migration


The generator will copy migrations from vendor/plugins/my_plugindb/migrate to db/migrate. It uses Rails functionality to ensure that the migration files are numbered sequentially starting with my application's next available migration number. If the migration is already in db/migrate, it will be skipped but it will not abort. This means I can add migrations to the plugin and run the generator again without worrying about it overwriting the old migrations.

Most of the work is done in the manifest method. By default, the Rails method migration_template looks for the template in a directory called template. In this case it would be looking for the migration file in vendor/plugins/my_plugin/generators/my_plugin/template which is what my relative_migration_path is relative to. My migration_exists method is basically a cut-and-paste from the Rails source code. I don't know why but they these methods are protected in Rails.

I hope this helps, it's a temporary fix. I'm sure the guys at plugin a week will have plugin_migrations fixed in no time :)

Thursday, December 6, 2007

Namespace Hell

I was writing a Rails plugin when I entered namespace hell. I had 2 classes with the same name and they were conflicting. I'm not sure if this was Rails problem or a simple ruby issue. Here is the scenario, the directory layout was as follows:

lib/query.rb
lib/warehouse/query.rb
lib/warehouse/table_report_query.rb

in query.rb I had:

class Query < ActiveRecord::Base
some_code_here
end


in lib/warehouse/query.rb I had:

module Warehouse
class Query
some_code_here
end

end


in lib/warehouse/table_report_query.rb I had:

module Warehouse
class TableReportQuery < Query
some_code_here
end
end

So, in the controller, when I did:

table_query = Warehouse::TableReportQuery.new()

You would think that TableReportQuery would inherit from Warehouse::Query. WRONG! It was inheriting from the first Query and throwing the classic activerecord "method not found" error.

So what's the solution. Well, I give credit to this blog article about Ruby Namespace Conflicts for inspiring my solution. I changed lib/warehouse/table_report_query.rb to be:

class Warehouse::TableReportQuery < Warehouse::Query
some_code_here
end

and it worked! Is that ruby Voodoo or what?

Tuesday, December 4, 2007

Plugin Development

Developing a Plugin for Rails can be a lot of fun. You can find a good guide here with the complete guide to rails plugins.

Piston

Be sure to read Managing Rails Plugins with Piston.

The best way to produce a good plugin is to create a separate application which can be used to test the plugin and use piston to import changes into you main application using 'piston update'.

Avoid Restarting Mongrel

I have learned one helpful trick. By default, you will have to restart your Mongrel server every time you make a change to your plugin. You can avoid this by placing the following code at the top of your plugin's init.rb file:

  Dependencies.load_once_paths.delete(lib_path)