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