Last active
April 5, 2016 12:25
-
-
Save movitto/dccbf8f570d1ce6b75db to your computer and use it in GitHub Desktop.
DB / Dia Diagram Comparison & Updater Tool
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/ruby | |
# Read / write db related metadata to/from dia diagram | |
# | |
# Copyright (C) 2015-2016 - Red Hat Inc. | |
require 'zlib' | |
require 'nokogiri' | |
require 'optparse' | |
require 'active_record' | |
require 'active_support/core_ext/string' | |
PATHS = { | |
:table => "//dia:diagram/dia:layer/dia:object[@type='UML - Class']", | |
:table_name => "dia:attribute[@name='name']/dia:string", | |
:table_pos => "dia:attribute[@name='obj_pos']/dia:point", | |
:table_color => "dia:attribute[@name='fill_color']/dia:color", | |
:connections => "//dia:diagram/dia:layer/dia:object[@type='Standard - Line' or @type='Standard - ZigZagLine']", | |
:connection_objs => "dia:connections/dia:connection", | |
:reverse_assoc => "dia:attribute[@name='start_arrow']" | |
} | |
ALIASES = { | |
"ext_management_systems" => "ems", | |
"vm_or_templates" => "vms", | |
"current_groups" => "miq_groups", | |
"for_users" => "users" | |
} | |
### | |
def config | |
@config ||= {} | |
end | |
def diagrams | |
config[:diagrams] ||= [] | |
end | |
def add_path(path) | |
diagrams << {:path => path} | |
end | |
def db_conf(conf=nil) | |
config[:db] ||= {} | |
config[:db].merge! conf if conf.is_a?(Hash) | |
config[:db] | |
end | |
def valid_diagrams? | |
!diagrams.empty? && diagrams.all? { |diagram| File.exist?(diagram[:path]) } | |
end | |
optparse = OptionParser.new do |opts| | |
opts.on('-h', '--help', 'Display this help screen') do | |
puts opts | |
exit | |
end | |
opts.on('-d', '--diagram diagram', 'Path to diagram to parse') do |path| | |
add_path path | |
end | |
opts.on('--db database', 'Database to connect to') do |db| | |
db_conf :database => db | |
end | |
opts.on('--db-adapter adapter', 'Type of database to connect to') do |adapter| | |
db_conf :adapter => adapter | |
end | |
opts.on('--db-user database_user', 'Database user to connect as') do |username| | |
db_conf :username => username | |
end | |
opts.on('--db-password database_password', 'Password to use when connecting to db') do |pass| | |
db_conf :pass => pass | |
end | |
end | |
optparse.parse! | |
### | |
def db_tables | |
config[:db_tables] ||= {} | |
end | |
def primary_tables | |
db_tables.select { |name,obj| !obj[:bridge] } | |
end | |
def bridge_tables | |
db_tables.select { |name,obj| obj[:bridge] } | |
end | |
def check_db? | |
!!db_conf[:adapter] && !!db_conf[:database] | |
end | |
def connect_to_db | |
begin | |
ActiveRecord::Base.establish_connection db_conf | |
rescue Exception => e | |
puts "Couldn't connect to db" | |
exit 1 | |
end | |
end | |
def skip_table?(name) | |
return (name =~ /^.+_rollups.*$/ || name =~ /^.+_[0-9]+$/) | |
end | |
def load_db_table(name) | |
return if skip_table?(name) | |
db_tables[name] ||= {:name => name, :relationships => [], :columns => []} | |
columns = ActiveRecord::Base.connection.columns(name) | |
columns.each { |c| | |
if c.name =~ /(.+)_id$/ | |
db_tables[name][:relationships] << {:name => $1.pluralize} | |
else | |
db_tables[name][:columns] << c.name | |
end | |
} | |
db_tables[name][:bridge] = (db_tables[name][:relationships].size == columns.size) | |
end | |
def load_db_relations | |
ActiveRecord::Base.connection.tables.each do |table_name| | |
load_db_table table_name | |
end | |
end | |
if check_db? | |
connect_to_db | |
load_db_relations | |
end | |
### | |
def dia_tables | |
config[:dia_tables] ||= {} | |
end | |
def dia_objs | |
config[:dia_objs] ||= {} | |
end | |
def unconnected | |
config[:unconnected] ||= [] | |
end | |
def inconsistent_names | |
names = dia_tables.keys | |
singularized = names.collect { |name| name.singularize }.uniq | |
plural = names - singularized | |
plural.select { |name| names.include?(name.singularize) } | |
end | |
def duplicates | |
dia_tables.select { |name,objs| objs.size > 1 } | |
end | |
def inconsistent_colors | |
duplicates.select { |name,objs| objs.collect { |obj| obj[:color] }.uniq.size > 1 || objs.first[:color] == '#ffffff' } | |
end | |
def current(diagram=nil) | |
unless diagram.nil? | |
reset_xml | |
config[:diagram] = nil | |
end | |
config[:diagram] ||= diagram | |
end | |
def xml | |
config[:xml] ||= Nokogiri::XML(current[:contents]) | |
end | |
def reset_xml | |
config[:xml] = nil | |
end | |
def uncompress | |
Zlib::GzipReader.open(current[:path]) do |uncompressed| | |
current[:contents] = uncompressed.read | |
end | |
end | |
def parse_dia_tables | |
xml.xpath(PATHS[:table]).each do |table| | |
dia_id = table['id'] | |
name = table.xpath(PATHS[:table_name]).first.inner_text | |
name.gsub!("#", "") | |
name.gsub!("(abstract)", "") | |
name.strip! | |
pos = table.xpath(PATHS[:table_pos]).first['val'] | |
x,y = pos.split(',') | |
color = table.xpath(PATHS[:table_color]).first['val'] | |
entry = {:table => name, :dia_id => dia_id, | |
:x => x, :y => y, :color => color, :relationships => []} | |
dia_tables[name] ||= [] | |
dia_tables[name] << entry | |
dia_objs[dia_id] = entry | |
end | |
end | |
def parse_dia_connections | |
xml.xpath(PATHS[:connections]).each do |line| | |
connected_nodes = [] | |
bridged = !line.xpath(PATHS[:reverse_assoc]).empty? | |
line.xpath(PATHS[:connection_objs]).collect do |connection| | |
to = connection['to'] | |
connected_to = dia_objs[to] | |
connected_nodes << connected_to unless connected_to.nil? | |
end | |
if connected_nodes.empty? | |
unconnected << line | |
elsif connected_nodes.size == 1 | |
unconnected << connected_nodes | |
else#if connected_nodes.size == 2 | |
connected_nodes.first[:relationships] << connected_nodes.last | |
connected_nodes.last[:relationships] << connected_nodes.first if bridged | |
end | |
end | |
end | |
diagrams.each { |diagram| | |
current diagram | |
uncompress | |
parse_dia_tables | |
parse_dia_connections | |
} | |
### | |
def missing_tables | |
@missing_tables ||= begin | |
dia_table_names = dia_tables.keys | |
db_table_names = primary_tables.keys | |
db_table_names.select { |table| !dia_table_names.include?(table) && | |
!dia_table_names.include?(table.singularize) && | |
!dia_table_names.include?(ALIASES[table]) } | |
end | |
end | |
def missing_relationships | |
@missing_relationships ||= begin | |
missing = {} | |
primary_tables.each { |name,db_obj| | |
dia_objs = dia_tables[name] | |
if dia_objs.nil? | |
db_obj[:relationships].each { |relationship| | |
missing[name] ||= [] | |
missing[name] << relationship[:name] | |
} | |
else | |
dia_relationships = dia_objs.collect { |obj| obj[:relationships] }.flatten.uniq | |
db_obj[:relationships].each { |relationship| | |
reference = relationship[:name] == "parents" ? name : relationship[:name] | |
has_relationship = !dia_relationships.select { |dia_rel| | |
dia_table_name = dia_rel[:table].pluralize | |
dia_table_name == reference || | |
dia_table_name == ALIASES[reference] | |
}.empty? | |
unless has_relationship | |
missing[name] ||= [] | |
missing[name] << relationship[:name] | |
end | |
} | |
end | |
} | |
missing.sort.to_h | |
end | |
end | |
def reverse_missing_relationships | |
@reverse_missing_relationships ||= begin | |
missing = {} | |
missing_relationships.each { |name, missing_relationships| | |
missing_relationships.each { |missing_table| | |
missing[missing_table] ||= [] | |
missing[missing_table] << name unless missing[missing_table].include?(name) | |
} | |
} | |
missing.sort.to_h | |
end | |
end | |
def incorrect_relationships | |
# ... | |
end | |
puts "===" | |
puts "Inconsistent Names:" | |
puts inconsistent_names | |
puts "===" | |
puts "Inconsistent Colors:" | |
inconsistent_colors.each { |name, objs| puts "#{name}: #{objs.size} #{objs.collect { |obj| obj[:color]}.uniq}" } | |
puts "===" | |
puts "Unconnected:" | |
puts unconnected | |
puts "===" | |
puts "Missing Tables:" | |
puts missing_tables | |
puts "===" | |
puts "Missing Relationships:" | |
puts missing_relationships | |
puts "===" | |
puts "Reverse Missing Relationships:" | |
puts reverse_missing_relationships | |
puts "===" | |
puts "Incorrect relationships:" | |
puts incorrect_relationships |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment