rails-erd 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,6 +28,12 @@ module RailsERD
28
28
  column.type
29
29
  end
30
30
 
31
+ # Returns +true+ if this attribute has no special meaning, that is, if it
32
+ # is not a primary key, foreign key, or timestamp.
33
+ def regular?
34
+ !primary_key? and !foreign_key? and !timestamp?
35
+ end
36
+
31
37
  # Returns +true+ if this attribute is mandatory. Mandatory attributes
32
38
  # either have a presence validation (+validates_presence_of+), or have a
33
39
  # <tt>NOT NULL</tt> database constraint.
@@ -65,32 +71,25 @@ module RailsERD
65
71
  name
66
72
  end
67
73
 
68
- # Returns a short description of the attribute type. If the attribute has
74
+ # Returns a description of the attribute type. If the attribute has
69
75
  # a non-standard limit or if it is mandatory, this information is included.
70
76
  #
71
77
  # Example output:
72
- # <tt>:integer</tt>:: int
73
- # <tt>:string, :limit => 255</tt>:: str
74
- # <tt>:string, :limit => 128</tt>:: str (128)
75
- # <tt>:boolean, :null => false</tt>:: bool *
78
+ # <tt>:integer</tt>:: integer
79
+ # <tt>:string, :limit => 255</tt>:: string
80
+ # <tt>:string, :limit => 128</tt>:: string (128)
81
+ # <tt>:boolean, :null => false</tt>:: boolean *
76
82
  def type_description
77
- case type
78
- when :integer then "int"
79
- when :float then "float"
80
- when :decimal then "dec"
81
- when :datetime then "datetime"
82
- when :date then "date"
83
- when :timestamp then "timest"
84
- when :time then "time"
85
- when :text then "txt"
86
- when :string then "str"
87
- when :binary then "blob"
88
- when :boolean then "bool"
89
- else type.to_s
90
- end.tap do |desc|
91
- desc << " (#{column.limit})" if column.limit != @model.connection.native_database_types[type][:limit]
83
+ type.to_s.tap do |desc|
84
+ desc << " (#{limit})" if limit
92
85
  desc << " ∗" if mandatory? # Add a hair space + low asterisk (Unicode characters).
93
86
  end
94
87
  end
88
+
89
+ # Returns any non-standard limit for this attribute. If a column has no
90
+ # limit or uses a default database limit, this method returns +nil+.
91
+ def limit
92
+ column.limit if column.limit != @model.connection.native_database_types[type][:limit]
93
+ end
95
94
  end
96
95
  end
@@ -12,25 +12,21 @@ module RailsERD
12
12
  # require "rails_erd/diagram"
13
13
  #
14
14
  # class YumlDiagram < RailsERD::Diagram
15
- # def process_relationship(rel)
16
- # return if rel.indirect?
15
+ # def process_relationship(relationship)
16
+ # return if relationship.indirect?
17
17
  #
18
18
  # arrow = case
19
- # when rel.cardinality.one_to_one? then "1-1>"
20
- # when rel.cardinality.one_to_many? then "1-*>"
21
- # when rel.cardinality.many_to_many? then "*-*>"
19
+ # when relationship.one_to_one? then "1-1>"
20
+ # when relationship.one_to_many? then "1-*>"
21
+ # when relationship.many_to_many? then "*-*>"
22
22
  # end
23
23
  #
24
- # instructions << "[#{rel.source}] #{arrow} [#{rel.destination}]"
24
+ # (@edges ||= []) << "[#{relationship.source}] #{arrow} [#{relationship.destination}]"
25
25
  # end
26
26
  #
27
27
  # def save
28
28
  # instructions * "\n"
29
29
  # end
30
- #
31
- # def instructions
32
- # @instructions ||= []
33
- # end
34
30
  # end
35
31
  #
36
32
  # Then, to generate the diagram (example based on the domain model of Gemcutter):
@@ -48,6 +44,22 @@ module RailsERD
48
44
  #
49
45
  # For another example implementation, see Diagram::Graphviz, which is the
50
46
  # default (and currently only) diagram type that is used by Rails ERD.
47
+ #
48
+ # === Options
49
+ #
50
+ # The following options are available and will by automatically used by any
51
+ # diagram generator inheriting from this class.
52
+ #
53
+ # attributes:: Selects which attributes to display. Can be any combination of
54
+ # +:regular+, +:primary_keys+, +:foreign_keys+, or +:timestamps+.
55
+ # disconnected:: Set to +false+ to exclude entities that are not connected to other
56
+ # entities. Defaults to +false+.
57
+ # indirect:: Set to +false+ to exclude relationships that are indirect.
58
+ # Indirect relationships are defined in Active Record with
59
+ # <tt>has_many :through</tt> associations.
60
+ # warn:: When set to +false+, no warnings are printed to the
61
+ # command line while processing the domain model. Defaults
62
+ # to +true+.
51
63
  class Diagram
52
64
  class << self
53
65
  # Generates a new domain model based on all <tt>ActiveRecord::Base</tt>
@@ -108,46 +120,34 @@ module RailsERD
108
120
  def process_relationship(relationship)
109
121
  end
110
122
 
111
- # Returns +true+ if the layout or hierarchy of the diagram should be
112
- # horizontally oriented.
113
- def horizontal?
114
- options.orientation == :horizontal
115
- end
116
-
117
- # Returns +true+ if the layout or hierarchy of the diagram should be
118
- # vertically oriented.
119
- def vertical?
120
- !horizontal?
121
- end
122
-
123
123
  private
124
124
 
125
125
  def filtered_entities
126
- @domain.entities.collect do |entity|
127
- if options.exclude_unconnected && !entity.connected?
128
- warn "Skipping unconnected model #{entity.name} (use exclude_unconnected=false to include)"
129
- else
130
- entity
131
- end
132
- end.compact.tap do |entities|
133
- raise "No (connected) entities found; create your models first!" if entities.empty?
126
+ @domain.entities.reject { |entity|
127
+ entity.descendant? or
128
+ !options.disconnected && entity.disconnected?
129
+ }.compact.tap do |entities|
130
+ raise "No entities found; create your models first!" if entities.empty?
134
131
  end
135
132
  end
136
133
 
137
134
  def filtered_relationships
138
- @domain.relationships
135
+ @domain.relationships.reject { |relationship|
136
+ relationship.source.descendant? or
137
+ relationship.destination.descendant? or
138
+ !options.indirect && relationship.indirect?
139
+ }
139
140
  end
140
141
 
141
142
  def filtered_attributes(entity)
142
- entity.attributes.reject { |attribute|
143
- options.exclude_primary_keys && attribute.primary_key? or
144
- options.exclude_foreign_keys && attribute.foreign_key? or
145
- options.exclude_timestamps && attribute.timestamp?
143
+ entity.attributes.select { |attribute|
144
+ # Select attributes that satisfy the conditions in the :attributes option.
145
+ options.attributes and [*options.attributes].any? { |type| attribute.send(:"#{type.to_s.chomp('s')}?") }
146
146
  }
147
147
  end
148
148
 
149
149
  def warn(message)
150
- puts "Warning: #{message}" unless options.suppress_warnings
150
+ puts "Warning: #{message}" if options.warn
151
151
  end
152
152
  end
153
153
  end
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
  require "rails_erd/diagram"
2
3
  require "graphviz"
3
4
  require "erb"
@@ -24,6 +25,26 @@ module RailsERD
24
25
  #
25
26
  # Please see the README.rdoc file for more details on how to use Rails ERD
26
27
  # from the command line.
28
+ #
29
+ # === Options
30
+ #
31
+ # The following options are supported:
32
+ #
33
+ # filename:: The file basename of the generated diagram. Defaults to +ERD+,
34
+ # or any other extension based on the file type.
35
+ # filetype:: The file type of the generated diagram. Defaults to +pdf+, which
36
+ # is the recommended format. Other formats may render significantly
37
+ # worse than a PDF file. The available formats depend on your installation
38
+ # of Graphviz.
39
+ # notation:: The cardinality notation to be used. Can be +:simple+ or
40
+ # +:advanced+. Refer to README.rdoc or to the examples on the project
41
+ # homepage for more information and examples.
42
+ # orientation:: The direction of the hierarchy of entities. Either +:horizontal+
43
+ # or +:vertical+. Defaults to +horizontal+. The orientation of the
44
+ # PDF that is generated depends on the amount of hierarchy
45
+ # in your models.
46
+ # title:: The title to add at the top of the diagram. Defaults to
47
+ # <tt>"YourApplication domain model"</tt>.
27
48
  class Graphviz < Diagram
28
49
  NODE_LABEL_TEMPLATE = ERB.new(File.read(File.expand_path("templates/node.erb", File.dirname(__FILE__))), nil, "<>") # @private :nodoc:
29
50
 
@@ -33,14 +54,16 @@ module RailsERD
33
54
  GRAPH_ATTRIBUTES = {
34
55
  :rankdir => :LR,
35
56
  :ranksep => 0.5,
36
- :nodesep => 0.35,
37
- :margin => "0.4,0.4",
57
+ :nodesep => 0.4,
58
+ :pad => "0.4,0.4",
59
+ :margin => "0,0",
38
60
  :concentrate => true,
39
61
  :labelloc => :t,
40
62
  :fontsize => 13,
41
63
  :fontname => "Arial Bold",
42
64
  :remincross => true,
43
- :outputorder => :edgesfirst }
65
+ :outputorder => :edgesfirst
66
+ }
44
67
 
45
68
  # Default node attributes.
46
69
  NODE_ATTRIBUTES = {
@@ -48,15 +71,54 @@ module RailsERD
48
71
  :fontsize => 10,
49
72
  :fontname => "Arial",
50
73
  :margin => "0.07,0.05",
51
- :penwidth => 0.8 }
74
+ :penwidth => 1.0
75
+ }
52
76
 
53
77
  # Default edge attributes.
54
78
  EDGE_ATTRIBUTES = {
55
79
  :fontname => "Arial",
56
80
  :fontsize => 8,
57
81
  :dir => :both,
58
- :arrowsize => 0.7,
59
- :penwidth => 0.8 }
82
+ :arrowsize => 0.9,
83
+ :penwidth => 1.0,
84
+ :labelangle => 32,
85
+ :labeldistance => 1.8,
86
+ :fontsize => 7
87
+ }
88
+
89
+ # Define different styles to draw the cardinality of relationships.
90
+ CARDINALITY_STYLES = {
91
+ # Closed arrows for to/from many.
92
+ :simple => lambda { |relationship, options|
93
+ options[:arrowhead] = relationship.to_many? ? :normal : :none
94
+ options[:arrowtail] = relationship.many_to? ? :normal : :none
95
+ },
96
+
97
+ # Closed arrow for to/from many, UML ranges at each end.
98
+ :uml => lambda { |relationship, options|
99
+ options[:arrowsize] = 0.7
100
+ options[:arrowhead] = relationship.to_many? ? :vee : :none
101
+ options[:arrowtail] = relationship.many_to? ? :vee : :none
102
+ ranges = [relationship.cardinality.destination_range, relationship.cardinality.source_range].map do |range|
103
+ if range.min == range.max
104
+ "#{range.min}"
105
+ else
106
+ "#{range.min}..#{range.max == Relationship::Cardinality::Infinity ? "∗" : range.max}"
107
+ end
108
+ end
109
+ options[:headlabel], options[:taillabel] = *ranges
110
+ },
111
+
112
+ # Arrow for to/from many, open or closed dots for optional/mandatory.
113
+ :advanced => lambda { |relationship, options|
114
+ dst = relationship.destination_optional? ? "odot" : "dot"
115
+ src = relationship.source_optional? ? "odot" : "dot"
116
+ dst << "normal" if relationship.to_many?
117
+ src << "normal" if relationship.many_to?
118
+ options[:arrowsize] = 0.6
119
+ options[:arrowhead], options[:arrowtail] = dst, src
120
+ }
121
+ }
60
122
 
61
123
  def graph
62
124
  @graph ||= GraphViz.digraph(@domain.name) do |graph|
@@ -65,19 +127,20 @@ module RailsERD
65
127
  NODE_ATTRIBUTES.each { |attribute, value| graph.node[attribute] = value }
66
128
  EDGE_ATTRIBUTES.each { |attribute, value| graph.edge[attribute] = value }
67
129
 
68
- # Switch rank direction if we're told to create a vertically
69
- # oriented graph.
130
+ # Switch rank direction if we're creating a vertically oriented graph.
70
131
  graph[:rankdir] = :TB if vertical?
71
132
 
72
133
  # Title of the graph itself.
73
- graph[:label] = "#{@domain.name} domain model\\n\\n"
134
+ graph[:label] = "#{title}\\n\\n" if title
74
135
  end
75
136
  end
76
137
 
77
138
  # Save the diagram and return the file name that was written to.
78
139
  def save
79
- graph.output(options.file_type.to_sym => file_name)
80
- file_name
140
+ graph.output(filetype => filename)
141
+ filename
142
+ rescue StandardError => e
143
+ raise "Saving diagram failed. Verify that Graphviz is installed or select filetype=dot."
81
144
  end
82
145
 
83
146
  protected
@@ -90,12 +153,39 @@ module RailsERD
90
153
  graph.add_edge graph.get_node(relationship.source.name), graph.get_node(relationship.destination.name),
91
154
  relationship_options(relationship)
92
155
  end
93
-
156
+
157
+ # Returns +true+ if the layout or hierarchy of the diagram should be
158
+ # horizontally oriented.
159
+ def horizontal?
160
+ options.orientation == :horizontal
161
+ end
162
+
163
+ # Returns +true+ if the layout or hierarchy of the diagram should be
164
+ # vertically oriented.
165
+ def vertical?
166
+ !horizontal?
167
+ end
168
+
94
169
  private
95
170
 
171
+ # Returns the title to be used for the graph.
172
+ def title
173
+ case options.title
174
+ when false then nil
175
+ when true then
176
+ if @domain.name then "#{@domain.name} domain model" else "Domain model" end
177
+ else options.title
178
+ end
179
+ end
180
+
96
181
  # Returns the file name that will be used when saving the diagram.
97
- def file_name
98
- "ERD.#{options.file_type}"
182
+ def filename
183
+ "#{options.filename}.#{options.filetype}"
184
+ end
185
+
186
+ # Returns the default file extension to be used when saving the diagram.
187
+ def filetype
188
+ if options.filetype.to_sym == :dot then :none else options.filetype.to_sym end
99
189
  end
100
190
 
101
191
  # Returns an options hash based on the given entity and its attributes.
@@ -105,14 +195,23 @@ module RailsERD
105
195
 
106
196
  # Returns an options hash
107
197
  def relationship_options(relationship)
108
- {}.tap do |options|
109
- options[:arrowhead] = relationship.cardinality.one_to_one? ? :dot : :normal
110
- options[:arrowtail] = relationship.cardinality.many_to_many? ? :normal : :dot
111
- options[:weight] = relationship.strength
112
- if relationship.indirect?
113
- options[:style] = :dotted
114
- options[:constraint] = false
115
- end
198
+ relationship_style_options(relationship).tap do |opts|
199
+ # Edges with a higher weight are optimised to be shorter and straighter.
200
+ opts[:weight] = relationship.strength
201
+
202
+ # Indirect relationships should not influence node ranks.
203
+ opts[:constraint] = false if relationship.indirect?
204
+ end
205
+ end
206
+
207
+ # Returns an options hash that defines the (cardinality) style for the
208
+ # relationship.
209
+ def relationship_style_options(relationship)
210
+ {}.tap do |opts|
211
+ opts[:style] = :dotted if relationship.indirect?
212
+
213
+ # Let cardinality style callbacks draw arrow heads and tails.
214
+ CARDINALITY_STYLES[options.notation][relationship, opts]
116
215
  end
117
216
  end
118
217
  end
@@ -2,16 +2,13 @@
2
2
  <table border="0" align="center" cellspacing="0.5" cellpadding="0" width="<%= NODE_WIDTH + 4 %>">
3
3
  <tr><td align="center" valign="bottom" width="<%= NODE_WIDTH %>"><font face="Arial Bold" point-size="11"><%= entity.name %></font></td></tr>
4
4
  </table>
5
+ <% if attributes.any? %>
5
6
  |
6
7
  <table border="0" align="left" cellspacing="2" cellpadding="0" width="<%= NODE_WIDTH + 4 %>">
7
- <% if attributes.any? %>
8
- <% attributes.each do |attribute| %>
9
- <tr>
10
- <td align="left" width="<%= NODE_WIDTH %>" port="<%= attribute %>"><%= attribute %> <font face="Arial Italic" color="grey60"><%= attribute.type_description %></font></td>
11
- </tr>
12
- <% end %>
13
- <% else %>
14
- <tr><td height="6"></td></tr>
15
- <% end %>
8
+ <% attributes.each do |attribute| %>
9
+ <tr><td align="left" width="<%= NODE_WIDTH %>" port="<%= attribute %>"><%= attribute %> <font face="Arial Italic" color="grey60"><%= attribute.type_description %></font></td></tr>
10
+ <% end %>
16
11
  </table>
12
+ <% else %>
13
+ <% end %>
17
14
  <% if vertical? %>}<% end %>
@@ -2,12 +2,19 @@ require "set"
2
2
  require "rails_erd"
3
3
  require "rails_erd/entity"
4
4
  require "rails_erd/relationship"
5
- require "rails_erd/relationship/cardinality"
6
5
  require "rails_erd/attribute"
7
6
 
8
7
  module RailsERD
9
8
  # The domain describes your Rails domain model. This class is the starting
10
9
  # point to get information about your models.
10
+ #
11
+ # === Options
12
+ #
13
+ # The following options are available:
14
+ #
15
+ # warn:: When set to +false+, no warnings are printed to the
16
+ # command line while processing the domain model. Defaults
17
+ # to +true+.
11
18
  class Domain
12
19
  class << self
13
20
  # Generates a domain model object based on all loaded subclasses of
@@ -38,7 +45,7 @@ module RailsERD
38
45
 
39
46
  # Returns all entities of your domain model.
40
47
  def entities
41
- @entities ||= entity_mapping.values.sort
48
+ @entities ||= Entity.from_models(self, @models)
42
49
  end
43
50
 
44
51
  # Returns all relationships in your domain model.
@@ -48,7 +55,7 @@ module RailsERD
48
55
 
49
56
  # Returns a specific entity object for the given Active Record model.
50
57
  def entity_for(model) # @private :nodoc:
51
- entity_mapping[model] or raise "model #{model} exists, but is not included in the domain"
58
+ entity_mapping[model] or raise "model #{model} exists, but is not included in domain"
52
59
  end
53
60
 
54
61
  # Returns an array of relationships for the given Active Record model.
@@ -61,10 +68,18 @@ module RailsERD
61
68
  [object_id << 1, relationships.map { |rel| "#{rel.source} => #{rel.destination}" } * ", "]
62
69
  end
63
70
 
71
+ def warn(message) # @private :nodoc:
72
+ puts "Warning: #{message}" if options.warn
73
+ end
74
+
64
75
  private
65
76
 
66
77
  def entity_mapping
67
- @entity_mapping ||= Hash[@models.collect { |model| [model, Entity.new(self, model)] }]
78
+ @entity_mapping ||= {}.tap do |mapping|
79
+ entities.each do |entity|
80
+ mapping[entity.model] = entity
81
+ end
82
+ end
68
83
  end
69
84
 
70
85
  def relationships_mapping
@@ -90,11 +105,7 @@ module RailsERD
90
105
  # Raises error if model is not in the domain.
91
106
  entity_for model
92
107
  rescue => e
93
- warn "Invalid association #{association_description(association)} (#{e.message})"
94
- end
95
-
96
- def warn(message)
97
- puts "Warning: #{message}" unless options.suppress_warnings
108
+ warn "Ignoring invalid association #{association_description(association)} (#{e.message})"
98
109
  end
99
110
 
100
111
  def association_description(association)