rails-erd 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)