app/controllers/erd/erd_controller.rb in erd-0.6.3 vs app/controllers/erd/erd_controller.rb in erd-0.6.4

- old
+ new

@@ -1,18 +1,15 @@ # frozen_string_literal: true require 'nokogiri' -require 'rails_erd/diagram/graphviz' +require 'ruby-graphviz' require 'erd/application_controller' module Erd class ErdController < ::Erd::ApplicationController def index - Rails.application.eager_load! - RailsERD.options[:filename], RailsERD.options[:filetype] = Rails.root.join('tmp/erd'), 'plain' - RailsERD::Diagram::Graphviz.create - plain = Rails.root.join('tmp/erd.plain').read + plain = generate_plain positions = if (json = Rails.root.join('tmp/erd_positions.json')).exist? ActiveSupport::JSON.decode json.read else {} end @@ -20,11 +17,11 @@ @migrations = Erd::Migrator.status end def update - changes = ActiveSupport::JSON.decode(params[:changes]) + changes = params[:changes].present? ? ActiveSupport::JSON.decode(params[:changes]) : [] executed_migrations, failed_migrations = [], [] changes.each do |row| begin action, model, column, from, to = row['action'], row['model'], row['column'], row['from'], row['to'] if action == 'move' @@ -77,30 +74,61 @@ Erd::Migrator.run_migrations :up => params[:up], :down => params[:down] redirect_to erd.root_path, :flash => {:executed_migrations => params.slice(:up, :down)} end private + + def generate_plain + if Rails.respond_to?(:autoloaders) && Rails.autoloaders.try(:zeitwerk_enabled?) + Zeitwerk::Loader.eager_load_all + else + Rails.application.eager_load! + end + ar_descendants = ActiveRecord::Base.descendants.reject {|m| m.name.in?(%w(ActiveRecord::SchemaMigration ActiveRecord::InternalMetadata ApplicationRecord)) } + ar_descendants.reject! {|m| !m.table_exists? } + + g = GraphViz.new('ERD', :type => :digraph, :rankdir => 'LR', :labelloc => :t, :ranksep => '1.5', :nodesep => '1.8', :margin => '0,0', :splines => 'spline') {|g| + nodes = ar_descendants.each_with_object({}) do |model, hash| + next if model.name.start_with? 'HABTM_' + hash[model.name] = model.columns.reject {|c| c.name.in? %w(id created_at updated_at) }.map {|c| [c.name, c.type]} + end + + edges = [] + ar_descendants.each do |model| + model.reflect_on_all_associations.each do |reflection| + next unless nodes.key? model.name + next if reflection.macro == :belongs_to + next unless nodes.key?(reflection.klass.name) + + edges << [model.name, reflection.klass.name] + # don't include the FKs in the diagram + nodes[reflection.klass.name].delete_if {|col, _type| col == reflection.foreign_key } + end + end + + nodes.each_pair do |model_name, cols| + g.add_nodes model_name, 'shape' => 'record', 'label' => "#{model_name}|#{cols.map {|name, type| "#{name}(#{type})"}.join('\l')}" + end + edges.each do |from, to| + g.add_edge g.search_node(from), g.search_node(to) + end + } + g.output('plain' => String) + end + def render_plain(plain, positions) - _scale, svg_width, svg_height = plain.scan(/\Agraph ([0-9\.]+) ([0-9\.]+) ([0-9\.]+)$/).first + _scale, svg_width, svg_height = plain.scan(/\Agraph ([\d\.]+) ([\d\.]+) ([\d\.]+)$/).first # node name x y width height label style shape color fillcolor max_model_x, max_model_y = 0, 0 - 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 = node_name.dup - model_name[0] = model_name[-1] = '' if (model_name.first == '"') && (model_name.last == '"') - model_name = model_name.sub(/^m_/, '') - next if model_name.in? ['ActiveRecord::SchemaMigration', 'ActiveRecord::InternalMetadata'] - 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 + models = plain.scan(/^node ([^ ]+) ([\d\.]+) ([\d\.]+) ([\d\.]+) ([\d\.]+) ([^ ]+) [^ ]+ [^ ]+ [^ ]+ [^ ]+\n/m).map {|model_name, x, y, width, height, label| + columns = label.gsub("\\\n", '').split('|')[1].split('\l').map {|name_and_type| name_and_type.scan(/(.*?)\((.*?)\)/).first }.map {|n, t| {:name => n, :type => t} } custom_x, custom_y = positions[model_name.tableize].try(:split, ',') h = {:model => model_name, :x => (custom_x || (BigDecimal(x) * 72).round), :y => (custom_y || (BigDecimal(y) * 72).round), :width => (BigDecimal(width) * 72).round, :height => height, :columns => columns} max_model_x, max_model_y = [h[:x].to_i + h[:width].to_i, max_model_x, 1024].max, [h[:y].to_i + h[:height].to_i, max_model_y, 768].max h }.compact # edge tail head n x1 y1 .. xn yn [label xl yl] style color - edges = plain.scan(/^edge ([^ ]+)+ ([^ ]+)/).map {|from, to| {:from => from.sub(/^m_/, ''), :to => to.sub(/^m_/, '')}} + edges = plain.scan(/^edge ([^ ]+)+ ([^ ]+)/).map {|from, to| {:from => from, :to => to}} render_to_string 'erd/erd/erd', :layout => nil, :locals => {:width => [(BigDecimal(svg_width) * 72).round, max_model_x].max, :height => [(BigDecimal(svg_height) * 72).round, max_model_y].max, :models => models, :edges => edges} end def gsub_file(path, flag, *args, &block) path = File.expand_path path, Rails.root