require "rails_erd/domain"
module RailsERD
# This class is an abstract class that will process a domain model and
# allows easy creation of diagrams. To implement a new diagram type, derive
# from this class and override +process_entity+, +process_relationship+,
# and (optionally) +save+.
#
# As an example, a diagram class that generates code that can be used with
# yUML (http://yuml.me) can be as simple as:
#
# require "rails_erd/diagram"
#
# class YumlDiagram < RailsERD::Diagram
# def process_relationship(relationship)
# return if relationship.indirect?
#
# arrow = case
# when relationship.one_to_one? then "1-1>"
# when relationship.one_to_many? then "1-*>"
# when relationship.many_to_many? then "*-*>"
# end
#
# (@edges ||= []) << "[#{relationship.source}] #{arrow} [#{relationship.destination}]"
# end
#
# def save
# instructions * "\n"
# end
# end
#
# Then, to generate the diagram (example based on the domain model of Gemcutter):
#
# YumlDiagram.create
# #=> "[Rubygem] 1-*> [Ownership]
# # [Rubygem] 1-*> [Subscription]
# # [Rubygem] 1-*> [Version]
# # [Rubygem] 1-1> [Linkset]
# # [Rubygem] 1-*> [Dependency]
# # [Version] 1-*> [Dependency]
# # [User] 1-*> [Ownership]
# # [User] 1-*> [Subscription]
# # [User] 1-*> [WebHook]"
#
# For another example implementation, see Diagram::Graphviz, which is the
# default (and currently only) diagram type that is used by Rails ERD.
#
# === Options
#
# The following options are available and will by automatically used by any
# diagram generator inheriting from this class.
#
# attributes:: Selects which attributes to display. Can be any combination of
# +:regular+, +:primary_keys+, +:foreign_keys+, or +:timestamps+.
# disconnected:: Set to +false+ to exclude entities that are not connected to other
# entities. Defaults to +false+.
# indirect:: Set to +false+ to exclude relationships that are indirect.
# Indirect relationships are defined in Active Record with
# has_many :through associations.
# warn:: When set to +false+, no warnings are printed to the
# command line while processing the domain model. Defaults
# to +true+.
class Diagram
class << self
# Generates a new domain model based on all ActiveRecord::Base
# subclasses, and creates a new diagram. Use the given options for both
# the domain generation and the diagram generation.
def create(options = {})
new(Domain.generate(options), options).create
end
end
# The options that are used to create this diagram.
attr_reader :options
# The domain that this diagram represents.
attr_reader :domain
# Create a new diagram based on the given domain.
def initialize(domain, options = {})
@domain, @options = domain, RailsERD.options.merge(options)
end
# Generates and saves the diagram, returning the result of +save+.
def create
generate
save
end
# Generates the diagram, but does not save the output. It is called
# internally by Diagram#create.
def generate
filtered_entities.each do |entity|
process_entity entity, filtered_attributes(entity)
end
filtered_relationships.each do |relationship|
process_relationship relationship
end
end
# Saves the diagram. Can be overridden in subclasses to write to an output
# file. It is called internally by Diagram#create.
def save
end
protected
# Process a given entity and its attributes. This method should be implemented
# by subclasses. It is intended to add a representation of the entity to
# the diagram. This method will be called once for each entity that should
# be displayed, typically in alphabetic order.
def process_entity(entity, attributes)
end
# Process a given relationship. This method should be implemented by
# subclasses. It should add a representation of the relationship to
# the diagram. This method will be called once for eacn relationship
# that should be displayed.
def process_relationship(relationship)
end
private
def filtered_entities
@domain.entities.reject { |entity|
entity.descendant? or
!options.disconnected && entity.disconnected?
}.compact.tap do |entities|
raise "No entities found; create your models first!" if entities.empty?
end
end
def filtered_relationships
@domain.relationships.reject { |relationship|
relationship.source.descendant? or
relationship.destination.descendant? or
!options.indirect && relationship.indirect?
}
end
def filtered_attributes(entity)
entity.attributes.select { |attribute|
# Select attributes that satisfy the conditions in the :attributes option.
options.attributes and [*options.attributes].any? { |type| attribute.send(:"#{type.to_s.chomp('s')}?") }
}
end
def warn(message)
puts "Warning: #{message}" if options.warn
end
end
end