require 'rails/generators' require 'nokogiri' require 'rails_erd/diagram/graphviz' require 'erd/application_controller' module Erd class MigrationError < StandardError; end class ErdController < ::Erd::ApplicationController def index # `bundle exec rake erd filename=tmp/erd filetype=plain` Rails.application.eager_load! RailsERD.options[:filename], RailsERD.options[:filetype] = 'tmp/erd', 'plain' RailsERD::Diagram::Graphviz.create plain = Rails.root.join('tmp/erd.plain').read positions = if (json = Rails.root.join('tmp/erd_positions.json')).exist? ActiveSupport::JSON.decode json.read else {} end @erd = render_plain plain, positions end def update changes = ActiveSupport::JSON.decode(params[:changes]) generated_migrations, failed_migrations = [], [] changes.each do |row| begin action, model, column, from, to = row['action'], row['model'].tableize, row['column'], row['from'], row['to'] before_migration_files = Dir.glob Rails.root.join('db', 'migrate', '*.rb') case action when 'remove_model' execute_generate_migration "drop_#{model}" generated_migration_file = (Dir.glob(Rails.root.join('db', 'migrate', '*.rb')) - before_migration_files).first gsub_file generated_migration_file, /def up.* end/m, "def change\n drop_table :#{model}\n end" generated_migrations << File.basename(generated_migration_file) when 'rename_model' from, to = from.tableize, to.tableize execute_generate_migration "rename_#{from}_to_#{to}" generated_migration_file = (Dir.glob(Rails.root.join('db', 'migrate', '*.rb')) - before_migration_files).first gsub_file generated_migration_file, /def up.* end/m, "def change\n rename_table :#{from}, :#{to}\n end" generated_migrations << File.basename(generated_migration_file) when 'add_column' name_and_type = column.scan(/(.*)\((.*?)\)/).first name, type = name_and_type[0], name_and_type[1] execute_generate_migration "add_#{name}_to_#{model}", ["#{name}:#{type}"] generated_migration_file = (Dir.glob(Rails.root.join('db', 'migrate', '*.rb')) - before_migration_files).first generated_migrations << File.basename(generated_migration_file) when 'rename_column' execute_generate_migration "rename_#{model}_#{from}_to_#{to}" generated_migration_file = (Dir.glob(Rails.root.join('db', 'migrate', '*.rb')) - before_migration_files).first gsub_file generated_migration_file, /def up.* end/m, "def change\n rename_column :#{model}, :#{from}, :#{to}\n end" generated_migrations << File.basename(generated_migration_file) when 'alter_column' execute_generate_migration "change_#{model}_#{column}_type_to_#{to}" generated_migration_file = (Dir.glob(Rails.root.join('db', 'migrate', '*.rb')) - before_migration_files).first gsub_file generated_migration_file, /def up.* end/m, "def change\n change_column :#{model}, :#{column}, :#{to}\n end" generated_migrations << File.basename(generated_migration_file) when 'move' json_file = Rails.root.join('tmp', 'erd_positions.json') positions = json_file.exist? ? ActiveSupport::JSON.decode(json_file.read) : {} positions[model] = to json_file.open('w') {|f| f.write positions.to_json} generated_migrations << 'saved entity positions' else raise "unexpected action: #{action}" end rescue ::Erd::MigrationError => e failed_migrations << e.message end end redirect_to erd.root_path, :flash => {:generated_migrations => generated_migrations, :failed_migrations => failed_migrations} end private def render_plain(plain, positions) _scale, svg_width, svg_height = plain.scan(/\Agraph ([0-9\.]+) ([0-9\.]+) ([0-9\.]+)$/).first ratio = [BigDecimal('4800') / BigDecimal(svg_width), BigDecimal('3200') / BigDecimal(svg_height), 180].min # node name x y width height label style shape color fillcolor models = plain.scan(/^node ([^ ]+) ([0-9\.]+) ([0-9\.]+) ([0-9\.]+) ([0-9\.]+) <\{?(<((?!^\}?>).)*)^\}?> [^ ]+ [^ ]+ [^ ]+ [^ ]+\n/m).map {|node_name, x, y, width, height, label| label_doc = Nokogiri::HTML::DocumentFragment.parse(label) model_name = label_doc.search('table')[0].search('tr > td').first.text next if model_name == 'ActiveRecord::SchemaMigration' columns = [] if (cols_table = label_doc.search('table')[1]) columns = cols_table.search('tr > td').map {|col| col_name, col_type = col.text.split(' '); {:name => col_name, :type => col_type}} end custom_x, custom_y = positions[model_name.tableize].try(:split, ',') {:model => model_name, :name => node_name, :x => (custom_x || BigDecimal(x) * ratio), :y => (custom_y || BigDecimal(y) * ratio), :width => BigDecimal(width) * ratio, :height => height, :columns => columns} }.compact # edge tail head n x1 y1 .. xn yn [label xl yl] style color edges = plain.scan(/^edge ([^ ]+)+ ([^ ]+)/).map {|from, to| {:from => from, :to => to}} render_to_string 'erd/erd/erd', :layout => nil, :locals => {:width => BigDecimal(svg_width) * ratio, :height => BigDecimal(svg_height) * ratio, :models => models, :edges => edges} end def execute_generate_migration(name, options = nil) overwriting_argv([name, options]) do Rails::Generators.configure! Rails.application.config.generators result = Rails::Generators.invoke 'migration', [name, options], :behavior => :invoke, :destination_root => Rails.root raise ::Erd::MigrationError, "#{name}#{"(#{options})" if options}" unless result end end # `rake db:migrate` def migrate ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_path, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) if ActiveRecord::Base.schema_format == :ruby File.open(ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb", "w") do |file| ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) end end end # a dirty workaround to make rspec-rails run def overwriting_argv(value, &block) original_argv = ARGV Object.const_set :ARGV, value block.call ensure Object.const_set :ARGV, original_argv end def gsub_file(path, flag, *args, &block) path = File.expand_path path, Rails.root content = File.read path content.gsub! flag, *args, &block File.open(path, 'w') {|file| file.write content} end end end