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.
- 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)
|