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.
- data/.gitignore +6 -2
- data/CHANGES.rdoc +16 -2
- data/Gemfile +16 -0
- data/Gemfile.lock +54 -0
- data/README.rdoc +18 -192
- data/Rakefile +58 -1
- data/VERSION +1 -1
- data/lib/rails_erd.rb +12 -30
- data/lib/rails_erd/attribute.rb +19 -20
- data/lib/rails_erd/diagram.rb +36 -36
- data/lib/rails_erd/diagram/graphviz.rb +121 -22
- data/lib/rails_erd/diagram/templates/node.erb +6 -9
- data/lib/rails_erd/domain.rb +20 -9
- data/lib/rails_erd/entity.rb +23 -0
- data/lib/rails_erd/relationship.rb +97 -8
- data/lib/rails_erd/relationship/cardinality.rb +107 -25
- data/lib/rails_erd/tasks.rake +3 -2
- data/rails-erd.gemspec +7 -5
- data/test/test_helper.rb +34 -8
- data/test/unit/attribute_test.rb +26 -4
- data/test/unit/cardinality_test.rb +105 -13
- data/test/unit/diagram_test.rb +170 -0
- data/test/unit/domain_test.rb +8 -8
- data/test/unit/entity_test.rb +72 -1
- data/test/unit/graphviz_test.rb +188 -30
- data/test/unit/rake_task_test.rb +45 -2
- data/test/unit/relationship_test.rb +273 -172
- metadata +6 -5
data/lib/rails_erd/attribute.rb
CHANGED
@@ -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
|
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>::
|
73
|
-
# <tt>:string, :limit => 255</tt>::
|
74
|
-
# <tt>:string, :limit => 128</tt>::
|
75
|
-
# <tt>:boolean, :null => false</tt>::
|
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
|
-
|
78
|
-
|
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
|
data/lib/rails_erd/diagram.rb
CHANGED
@@ -12,25 +12,21 @@ module RailsERD
|
|
12
12
|
# require "rails_erd/diagram"
|
13
13
|
#
|
14
14
|
# class YumlDiagram < RailsERD::Diagram
|
15
|
-
# def process_relationship(
|
16
|
-
# return if
|
15
|
+
# def process_relationship(relationship)
|
16
|
+
# return if relationship.indirect?
|
17
17
|
#
|
18
18
|
# arrow = case
|
19
|
-
# when
|
20
|
-
# when
|
21
|
-
# when
|
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
|
-
#
|
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.
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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.
|
143
|
-
|
144
|
-
options.
|
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}"
|
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.
|
37
|
-
:
|
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
|
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.
|
59
|
-
:penwidth => 0
|
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
|
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] = "#{
|
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(
|
80
|
-
|
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
|
98
|
-
"
|
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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
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 %>
|
data/lib/rails_erd/domain.rb
CHANGED
@@ -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 ||=
|
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
|
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 ||=
|
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 "
|
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)
|