Skip to content

Instantly share code, notes, and snippets.

@movitto
Last active April 5, 2016 12:25
Show Gist options
  • Save movitto/dccbf8f570d1ce6b75db to your computer and use it in GitHub Desktop.
Save movitto/dccbf8f570d1ce6b75db to your computer and use it in GitHub Desktop.
DB / Dia Diagram Comparison & Updater Tool
#!/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