-
# encoding: utf-8
-
-
# mixin Graphviz name space (what ever it belongs)
-
1
module Graphviz
-
# A name space for all types of diagram figures
-
1
module Diagram
-
# An UML class diagram
-
1
class ClassDiagram
-
1
attr_reader :graphviz
-
-
# A entity class with many attributes and methods definition strings
-
1
class Entity
-
# The `attributes` and `methods` are lists of Hashes contain
-
# information necessary to produce Graphviz labels, such as
-
# `name`, `visibility`, `arguments` (for methods) and `type`.
-
#
-
# A key `field_id` will generated in a auto-increment manner
-
# if a relationship (e.g., belongs to) are attached to a specific
-
# member field.
-
#
-
# `node_attributes` are Graphviz attributes for the node represents
-
# the entity.
-
1
attr_accessor :name, :attributes, :methods, :node_attributes
-
1
def initialize(name, attrs = {})
-
3
@name, @attributes, @methods = name, [], []
-
3
@node_attributes = { shape: 'record' }.merge(attrs)
-
end
-
-
1
def member(name)
-
members.each { |m| return m if m[:name] == name }
-
end
-
-
1
def attribute(name)
-
attributes.each { |m| return m if m[:name] == name }
-
end
-
-
1
def method(name)
-
methods.each { |m| return m if m[:name] == name }
-
end
-
-
# All member fields include both attributes and methods
-
1
def members
-
@attributes + @methods
-
end
-
-
1
def add_attribute(name, opts = {})
-
6
opts[:name] = name
-
6
opts[:visibility] ||= :public
-
6
opts.keys.each do |k|
-
13
known_keys = [:name, :visibility, :type, :field_id]
-
13
fail "unknown attribute #{k}" unless known_keys.include?(k.to_sym)
-
end
-
6
@attributes << opts
-
end
-
-
1
def add_method(name, opts = {})
-
3
opts[:name] = name
-
3
opts[:visibility] ||= :public
-
3
opts.keys.each do |k|
-
8
known_keys = [:name, :visibility, :arguments, :type, :field_id]
-
8
fail "unknown attribute #{k}" unless known_keys.include?(k.to_sym)
-
end
-
3
@methods << opts
-
end
-
-
1
def belongs_to(entity)
-
Aggragation.new self, entity
-
end
-
-
1
def embedded_in(entity)
-
Composition.new self, entity
-
end
-
-
1
def extends(entity)
-
Generalization.new self, entity
-
end
-
-
1
def implements(entity)
-
Realization.new self, entity
-
end
-
-
# rubocop:disable MethodLength
-
1
def label
-
1
rl = RecordLabel.new
-
1
rl.add_row(name)
-
1
rl.add_separator
-
1
attributes.each do |at|
-
2
str = format '%s%s', visibility_symbol(at), at[:name]
-
2
str = "#{str} : #{at[:type].capitalize}" if at[:type]
-
2
rl.add_row str, align: :left, field_id: at[:field_id]
-
end
-
-
1
rl.add_separator
-
1
methods.each do |at|
-
arguments = at[:arguments] ? at[:arguments] : ''
-
str = format '%s%s(%s)', visibility_symbol(at), at[:name],
-
arguments.sub(/\A\(/, '').sub(/\)\Z/, '')
-
str = "#{str} : #{at[:type].capitalize}" if at[:type]
-
rl.add_row str, align: :left, field_id: at[:field_id]
-
end
-
-
1
rl.to_s
-
end
-
-
1
private
-
-
1
def visibility_symbol(key)
-
2
case key[:visibility].to_sym
-
1
when :private then '- '
-
when :protected then '# '
-
when :derived then '/ '
-
when :package then '~ '
-
else
-
1
'+ '
-
end
-
end
-
end
-
-
# Relationship between entities
-
1
class Link
-
# From and to are entities links each other
-
1
attr_accessor :from, :to, :attributes
-
-
1
def initialize(from, to, attributes = {})
-
attrs = default_attrs.merge(attributes)
-
@from, @to, @attributes = from, to, attrs
-
end
-
-
1
def default_attrs
-
{}
-
end
-
end
-
-
# :from belongs to :to
-
1
class Aggragation < Link
-
1
def default_attrs
-
{ arrowhead: 'diamond', headlabel: '1', taillabel: '0..*' }
-
end
-
end
-
-
# :from embeds in :to
-
1
class Composition < Link
-
1
def default_attrs
-
{ arrowhead: 'odiamond', headlabel: '1', taillabel: '0..*' }
-
end
-
end
-
-
# :from extends (sub-classing) :to
-
1
class Generalization < Link
-
1
def default_attrs
-
{ arrowhead: 'empty' }
-
end
-
end
-
-
# :from implement :to (interface)
-
1
class Realization < Link
-
1
def default_attrs
-
{ style: 'dashed', arrowhead: 'vee', label: 'implements' }
-
end
-
end
-
-
# All `opts` are passed to the constructor of `GraphViz`
-
1
def initialize(opts = {})
-
3
opts[:type] ||= :digraph
-
3
opts[:rankdir] ||= 'LR'
-
3
@graphviz = GraphViz.new :G, opts
-
3
@entities, @links = [], []
-
end
-
-
1
def <<(obj)
-
case obj
-
when Link then @links << obj
-
when Entity then @entities << obj
-
else
-
fail "unknown type #{obj.class}, can not add to graph"
-
end
-
end
-
-
1
def add_entity(name, opts = {})
-
self << Entity.new(name, opts)
-
end
-
-
1
def entity(name)
-
@entities.each { |e| return e if e.name == name }
-
nil
-
end
-
-
1
def [](name)
-
add_entity(name) unless entity(name)
-
entity(name)
-
end
-
-
1
def output(hsh)
-
@entities.each do |e|
-
attrs = { label: e.label }.merge(e.node_attributes)
-
@graphviz.add_node(e.name, attrs)
-
end
-
-
@links.each do |l|
-
@graphviz.add_edges(l.from.name, l.to.name, l.attributes)
-
end
-
-
@graphviz.output hsh
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
-
# mixin Graphviz name space (what ever it belongs)
-
1
module Graphviz
-
# A name space for all types of diagram figures
-
1
module Diagram
-
# Provide an interface for create Graphviz record type labels
-
1
class RecordLabel
-
1
def initialize
-
6
@rows = [] # list of Strings
-
end
-
-
# rubocop:disable MethodLength
-
1
def add_row(string, opts = {})
-
15
str = quote(string).chomp
-
15
str = format '<%s> %s', opts[:field_id], str if opts[:field_id]
-
15
if opts[:align]
-
4
case opts[:align].to_sym
-
3
when :left then @rows << str + ' \l'
-
1
when :right then @rows << str + ' \r'
-
else
-
fail "unsupported align #{opts[:align]}"
-
end
-
else
-
11
@rows << str + "\n"
-
end
-
end
-
-
1
def add_separator
-
5
@rows << '|'
-
end
-
-
1
def to_s
-
6
@rows.join('').chomp.gsub("\n|", '|')
-
end
-
-
1
private
-
-
1
def quote(label)
-
15
label.gsub(/\[\]\{\}\(\)\s/) { |w| '\\' + w }
-
end
-
end
-
end
-
end