Class: RailsERD::Diagram

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_erd/diagram.rb,
lib/rails_erd/diagram/tbls.rb,
lib/rails_erd/diagram/mermaid.rb,
lib/rails_erd/diagram/graphviz.rb

Overview

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 (https://yuml.me) can be as simple as:

require "rails_erd/diagram"

class YumlDiagram < RailsERD::Diagram
setup { @edges = [] }

each_relationship do |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

save { @edges * "\n" }
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 :content, :primary_keys, :foreign_keys, :timestamps, or :inheritance.

only_attributes

Shows only the listed attributes for the given models, without affecting other models. Accepts a hash that maps model names to a list of attribute names to keep. From the command line it can also be given as a comma separated string of Model.attribute entries, for example only_attributes="Book.title,Book.isbn". Models that are not listed keep all of their attributes. Unlike exclude_attributes it cannot be set to true.

exclude_attributes

Hides attributes on a per-model basis, without affecting other models. Accepts a hash that maps model names to either true (hide all attributes for that model) or a list of attribute names to hide. From the command line it can also be given as a comma separated string where each entry is either Model (hide all attributes) or Model.attribute (hide a single attribute), for example exclude_attributes="BigTable,User.password_digest". Applied after only_attributes.

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.

inheritance

Set to true to include specializations, which correspond to Rails single table inheritance.

polymorphism

Set to true to include generalizations, which correspond to Rails polymorphic associations.

warn

When set to false, no warnings are printed to the command line while processing the domain model. Defaults to true.

Defined Under Namespace

Classes: Graphviz, Mermaid, Tbls

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(domain, options = {}) ⇒ Diagram

Create a new diagram based on the given domain.



198
199
200
# File 'lib/rails_erd/diagram.rb', line 198

def initialize(domain, options = {})
  @domain, @options = domain, RailsERD.options.merge(options)
end

Instance Attribute Details

#domainObject (readonly)

The domain that this diagram represents.



195
196
197
# File 'lib/rails_erd/diagram.rb', line 195

def domain
  @domain
end

#optionsObject (readonly)

The options that are used to create this diagram.



192
193
194
# File 'lib/rails_erd/diagram.rb', line 192

def options
  @options
end

Class Method Details

.create(options = {}) ⇒ Object

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.



92
93
94
# File 'lib/rails_erd/diagram.rb', line 92

def create(options = {})
  new(Domain.generate(options), options).create
end

.normalize_attribute_filter(value) ⇒ Object

Canonicalises an attribute filter option (+only_attributes+ or exclude_attributes) into a hash that maps model names (as strings) to either true (the whole model was named, without any attribute) or an array of attribute names. Accepts several input shapes:

* a hash, e.g. <tt>{ "BigTable" => true, "User" => ["password_digest"] }</tt>
(values may be +true+/+"all"+ to name the whole model, +false+/+nil+
to name no attributes, or a comma separated string / array of names);
* a string or array of <tt>Model</tt> / <tt>Model.attribute</tt>
entries, e.g. <tt>"BigTable,User.password_digest"</tt>.

How true and an empty list are interpreted is up to the caller: for exclude_attributes they mean "hide every attribute" and "hide none", for only_attributes they both mean "keep every attribute".



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/rails_erd/diagram.rb', line 110

def normalize_attribute_filter(value)
  return {} if value.nil? || value == false

  if value.is_a?(Hash)
    value.each_with_object({}) do |(model, attrs), result|
      result[model.to_s] = normalize_attribute_filter_value(attrs)
    end
  else
    entries = Array(value).flat_map { |entry| entry.to_s.split(",") }
    entries.each_with_object({}) do |entry, result|
      model, attribute = entry.split(".", 2)
      model = model.to_s.strip
      next if model.empty?

      if attribute.nil?
        result[model] = true
      elsif result[model] != true
        (result[model] ||= []) << attribute.strip
      end
    end
  end
end

.normalize_exclude_attributes(value) ⇒ Object

Canonicalises the exclude_attributes option. See #normalize_attribute_filter.



148
149
150
# File 'lib/rails_erd/diagram.rb', line 148

def normalize_exclude_attributes(value)
  normalize_attribute_filter(value)
end

.normalize_only_attributes(value) ⇒ Object

Canonicalises the only_attributes option. See #normalize_attribute_filter. Unlike exclude_attributes it cannot be enabled with a bare true: "show only all attributes" has no meaning, so it is rejected rather than silently ignored.



137
138
139
140
141
142
143
144
145
# File 'lib/rails_erd/diagram.rb', line 137

def normalize_only_attributes(value)
  if value == true
    raise ArgumentError, "only_attributes cannot be true: it takes the attributes to keep, " \
      "either as a hash of model names to attribute names, or as a list of Model.attribute " \
      "entries. Use attributes: false or exclude_attributes to hide attributes."
  end

  normalize_attribute_filter(value)
end

Instance Method Details

#createObject

Generates and saves the diagram, returning the result of save.



203
204
205
206
# File 'lib/rails_erd/diagram.rb', line 203

def create
  generate
  save
end

#generateObject

Generates the diagram, but does not save the output. It is called internally by Diagram#create.



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/rails_erd/diagram.rb', line 210

def generate
  instance_eval(&callbacks[:setup])
  if options.only_recursion_depth.present?
    depth = options.only_recursion_depth.to_s.to_i
    # Ensure options[:only] is an array (CLI may pass a single string)
    options[:only] = [options[:only]].flatten
    options[:only].dup.each do |class_name|
      options[:only] += recurse_into_relationships(@domain.entity_by_name(class_name), depth)
    end
    options[:only].uniq!
  end

  filtered_entities.each do |entity|
    instance_exec entity, filtered_attributes(entity), &callbacks[:each_entity]
  end

  filtered_specializations.each do |specialization|
    instance_exec specialization, &callbacks[:each_specialization]
  end

  filtered_relationships.each do |relationship|
    instance_exec relationship, &callbacks[:each_relationship]
  end
end

#recurse_into_relationships(entity, max_level, current_level = 0) ⇒ Object



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/rails_erd/diagram.rb', line 235

def recurse_into_relationships(entity, max_level, current_level = 0)
  return [] unless entity
  return [] if max_level == current_level

  relationships = entity.relationships.reject{|r| r.indirect? || r.recursive?}

  relationships.map do |relationship|
    other_entitiy = if relationship.source == entity
                      relationship.destination
                    else
                      relationship.source
                    end
    if other_entitiy and !other_entitiy.generalized?
      [other_entitiy.name] + recurse_into_relationships(other_entitiy, max_level, current_level + 1)
    else
      []
    end
  end.flatten.uniq
end

#saveObject



255
256
257
# File 'lib/rails_erd/diagram.rb', line 255

def save
  instance_eval(&callbacks[:save])
end