jw-rails-erd 1.4.5
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.
- checksums.yaml +7 -0
- data/README.md +86 -0
- data/Rakefile +20 -0
- data/bin/erd +4 -0
- data/lib/generators/erd/USAGE +4 -0
- data/lib/generators/erd/install_generator.rb +14 -0
- data/lib/generators/erd/templates/auto_generate_diagram.rake +6 -0
- data/lib/rails-erd.rb +1 -0
- data/lib/rails_erd/cli.rb +164 -0
- data/lib/rails_erd/config.rb +97 -0
- data/lib/rails_erd/custom.rb +99 -0
- data/lib/rails_erd/diagram/graphviz.rb +295 -0
- data/lib/rails_erd/diagram/templates/node.html.erb +14 -0
- data/lib/rails_erd/diagram/templates/node.record.erb +4 -0
- data/lib/rails_erd/diagram.rb +188 -0
- data/lib/rails_erd/domain/attribute.rb +160 -0
- data/lib/rails_erd/domain/entity.rb +104 -0
- data/lib/rails_erd/domain/relationship/cardinality.rb +118 -0
- data/lib/rails_erd/domain/relationship.rb +203 -0
- data/lib/rails_erd/domain/specialization.rb +90 -0
- data/lib/rails_erd/domain.rb +153 -0
- data/lib/rails_erd/railtie.rb +10 -0
- data/lib/rails_erd/tasks.rake +58 -0
- data/lib/rails_erd/version.rb +4 -0
- data/lib/rails_erd.rb +73 -0
- data/lib/tasks/auto_generate_diagram.rake +21 -0
- data/test/support_files/erdconfig.another_example +3 -0
- data/test/support_files/erdconfig.example +19 -0
- data/test/support_files/erdconfig.exclude.example +19 -0
- data/test/test_helper.rb +160 -0
- data/test/unit/attribute_test.rb +316 -0
- data/test/unit/cardinality_test.rb +123 -0
- data/test/unit/config_test.rb +110 -0
- data/test/unit/diagram_test.rb +352 -0
- data/test/unit/domain_test.rb +258 -0
- data/test/unit/entity_test.rb +252 -0
- data/test/unit/graphviz_test.rb +461 -0
- data/test/unit/rake_task_test.rb +174 -0
- data/test/unit/relationship_test.rb +476 -0
- data/test/unit/specialization_test.rb +67 -0
- metadata +155 -0
@@ -0,0 +1,295 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "rails_erd/diagram"
|
3
|
+
require "graphviz"
|
4
|
+
require "erb"
|
5
|
+
|
6
|
+
# Fix bad RegEx test in Ruby-Graphviz.
|
7
|
+
GraphViz::Types::LblString.class_eval do
|
8
|
+
def output # @private :nodoc:
|
9
|
+
if /^<.*>$/m =~ @data
|
10
|
+
@data
|
11
|
+
else
|
12
|
+
@data.to_s.inspect.gsub("\\\\", "\\")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
alias_method :to_gv, :output
|
16
|
+
alias_method :to_s, :output
|
17
|
+
end
|
18
|
+
|
19
|
+
module RailsERD
|
20
|
+
class Diagram
|
21
|
+
# Create Graphviz-based diagrams based on the domain model. For easy
|
22
|
+
# command line graph generation, you can use:
|
23
|
+
#
|
24
|
+
# % rake erd
|
25
|
+
#
|
26
|
+
# === Options
|
27
|
+
#
|
28
|
+
# The following options are supported:
|
29
|
+
#
|
30
|
+
# filename:: The file basename of the generated diagram. Defaults to +ERD+,
|
31
|
+
# or any other extension based on the file type.
|
32
|
+
# filetype:: The file type of the generated diagram. Defaults to +pdf+, which
|
33
|
+
# is the recommended format. Other formats may render significantly
|
34
|
+
# worse than a PDF file. The available formats depend on your installation
|
35
|
+
# of Graphviz.
|
36
|
+
# notation:: The cardinality notation to be used. Can be +:simple+ or
|
37
|
+
# +:bachman+. Refer to README.rdoc or to the examples on the project
|
38
|
+
# homepage for more information and examples.
|
39
|
+
# orientation:: The direction of the hierarchy of entities. Either +:horizontal+
|
40
|
+
# or +:vertical+. Defaults to +horizontal+. The orientation of the
|
41
|
+
# PDF that is generated depends on the amount of hierarchy
|
42
|
+
# in your models.
|
43
|
+
# title:: The title to add at the top of the diagram. Defaults to
|
44
|
+
# <tt>"YourApplication domain model"</tt>.
|
45
|
+
class Graphviz < Diagram
|
46
|
+
NODE_LABEL_TEMPLATES = {
|
47
|
+
html: "node.html.erb",
|
48
|
+
record: "node.record.erb"
|
49
|
+
} # @private :nodoc:
|
50
|
+
|
51
|
+
NODE_WIDTH = 130 # @private :nodoc:
|
52
|
+
|
53
|
+
FONTS = Config.font_names_based_on_os
|
54
|
+
|
55
|
+
# Default graph attributes.
|
56
|
+
GRAPH_ATTRIBUTES = {
|
57
|
+
rankdir: :LR,
|
58
|
+
ranksep: 0.5,
|
59
|
+
nodesep: 0.4,
|
60
|
+
pad: "0.4,0.4",
|
61
|
+
margin: "0,0",
|
62
|
+
concentrate: true,
|
63
|
+
labelloc: :t,
|
64
|
+
fontsize: 13,
|
65
|
+
fontname: FONTS[:bold]
|
66
|
+
}
|
67
|
+
|
68
|
+
# Default node attributes.
|
69
|
+
NODE_ATTRIBUTES = {
|
70
|
+
shape: "Mrecord",
|
71
|
+
fontsize: 10,
|
72
|
+
fontname: FONTS[:normal],
|
73
|
+
margin: "0.07,0.05",
|
74
|
+
penwidth: 1.0
|
75
|
+
}
|
76
|
+
|
77
|
+
# Default edge attributes.
|
78
|
+
EDGE_ATTRIBUTES = {
|
79
|
+
fontname: FONTS[:normal],
|
80
|
+
fontsize: 7,
|
81
|
+
dir: :both,
|
82
|
+
arrowsize: 0.9,
|
83
|
+
penwidth: 1.0,
|
84
|
+
labelangle: 32,
|
85
|
+
labeldistance: 1.8,
|
86
|
+
}
|
87
|
+
|
88
|
+
module Simple
|
89
|
+
def entity_style(entity, attributes)
|
90
|
+
{}.tap do |options|
|
91
|
+
options[:fontcolor] = options[:color] = :grey60 if entity.virtual?
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def relationship_style(relationship)
|
96
|
+
{}.tap do |options|
|
97
|
+
options[:style] = :dotted if relationship.indirect?
|
98
|
+
|
99
|
+
# Closed arrows for to/from many.
|
100
|
+
options[:arrowhead] = relationship.to_many? ? "normal" : "none"
|
101
|
+
options[:arrowtail] = relationship.many_to? ? "normal" : "none"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def specialization_style(specialization)
|
106
|
+
{ color: :grey60,
|
107
|
+
arrowtail: :onormal,
|
108
|
+
arrowhead: :none,
|
109
|
+
arrowsize: 1.2 }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
module Crowsfoot
|
114
|
+
include Simple
|
115
|
+
def relationship_style(relationship)
|
116
|
+
{}.tap do |options|
|
117
|
+
options[:style] = :dotted if relationship.indirect?
|
118
|
+
|
119
|
+
# Cardinality is "look-across".
|
120
|
+
dst = relationship.to_many? ? "crow" : "tee"
|
121
|
+
src = relationship.many_to? ? "crow" : "tee"
|
122
|
+
|
123
|
+
# Participation is "look-across".
|
124
|
+
dst << (relationship.destination_optional? ? "odot" : "tee")
|
125
|
+
src << (relationship.source_optional? ? "odot" : "tee")
|
126
|
+
|
127
|
+
options[:arrowsize] = 0.6
|
128
|
+
options[:arrowhead], options[:arrowtail] = dst, src
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
module Bachman
|
134
|
+
include Simple
|
135
|
+
def relationship_style(relationship)
|
136
|
+
{}.tap do |options|
|
137
|
+
options[:style] = :dotted if relationship.indirect?
|
138
|
+
|
139
|
+
# Participation is "look-here".
|
140
|
+
dst = relationship.source_optional? ? "odot" : "dot"
|
141
|
+
src = relationship.destination_optional? ? "odot" : "dot"
|
142
|
+
|
143
|
+
# Cardinality is "look-across".
|
144
|
+
dst << "normal" if relationship.to_many?
|
145
|
+
src << "normal" if relationship.many_to?
|
146
|
+
|
147
|
+
options[:arrowsize] = 0.6
|
148
|
+
options[:arrowhead], options[:arrowtail] = dst, src
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
module Uml
|
154
|
+
include Simple
|
155
|
+
def relationship_style(relationship)
|
156
|
+
{}.tap do |options|
|
157
|
+
options[:style] = :dotted if relationship.indirect?
|
158
|
+
|
159
|
+
options[:arrowsize] = 0.7
|
160
|
+
options[:arrowhead] = relationship.to_many? ? "vee" : "none"
|
161
|
+
options[:arrowtail] = relationship.many_to? ? "vee" : "none"
|
162
|
+
|
163
|
+
ranges = [relationship.cardinality.destination_range, relationship.cardinality.source_range].map do |range|
|
164
|
+
if range.min == range.max
|
165
|
+
"#{range.min}"
|
166
|
+
else
|
167
|
+
"#{range.min}..#{range.max == Domain::Relationship::N ? "∗" : range.max}"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
options[:headlabel], options[:taillabel] = *ranges
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
attr_accessor :graph
|
176
|
+
|
177
|
+
setup do
|
178
|
+
self.graph = GraphViz.digraph(domain.name)
|
179
|
+
|
180
|
+
# Set all default attributes.
|
181
|
+
GRAPH_ATTRIBUTES.each { |attribute, value| graph[attribute] = value }
|
182
|
+
NODE_ATTRIBUTES.each { |attribute, value| graph.node[attribute] = value }
|
183
|
+
EDGE_ATTRIBUTES.each { |attribute, value| graph.edge[attribute] = value }
|
184
|
+
|
185
|
+
# Switch rank direction if we're creating a vertically oriented graph.
|
186
|
+
graph[:rankdir] = :TB if options.orientation == :vertical
|
187
|
+
|
188
|
+
# Title of the graph itself.
|
189
|
+
graph[:label] = "#{title}\\n\\n" if title
|
190
|
+
|
191
|
+
# Setup notation options.
|
192
|
+
extend self.class.const_get(options.notation.to_s.capitalize.to_sym)
|
193
|
+
end
|
194
|
+
|
195
|
+
save do
|
196
|
+
raise "Saving diagram failed!\nOutput directory '#{File.dirname(filename)}' does not exist." unless File.directory?(File.dirname(filename))
|
197
|
+
|
198
|
+
begin
|
199
|
+
# GraphViz doesn't like spaces in the filename
|
200
|
+
graph.output(filetype => filename.gsub(/\s/,"_"))
|
201
|
+
filename
|
202
|
+
rescue RuntimeError => e
|
203
|
+
raise "Saving diagram failed!\nGraphviz produced errors. Verify it " +
|
204
|
+
"has support for filetype=#{options.filetype}, or use " +
|
205
|
+
"filetype=dot.\nOriginal error: #{e.message.split("\n").last}"
|
206
|
+
rescue StandardError => e
|
207
|
+
raise "Saving diagram failed!\nVerify that Graphviz is installed " +
|
208
|
+
"and in your path, or use filetype=dot."
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
each_entity do |entity, attributes|
|
213
|
+
draw_node entity.name, entity_options(entity, attributes)
|
214
|
+
end
|
215
|
+
|
216
|
+
each_specialization do |specialization|
|
217
|
+
from, to = specialization.generalized, specialization.specialized
|
218
|
+
draw_edge from.name, to.name, specialization_options(specialization)
|
219
|
+
end
|
220
|
+
|
221
|
+
each_relationship do |relationship|
|
222
|
+
from, to = relationship.source, relationship.destination
|
223
|
+
unless draw_edge from.name, to.name, relationship_options(relationship)
|
224
|
+
from.children.each do |child|
|
225
|
+
draw_edge child.name, to.name, relationship_options(relationship)
|
226
|
+
end
|
227
|
+
to.children.each do |child|
|
228
|
+
draw_edge from.name, child.name, relationship_options(relationship)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
private
|
234
|
+
|
235
|
+
def node_exists?(name)
|
236
|
+
!!graph.get_node(escape_name(name))
|
237
|
+
end
|
238
|
+
|
239
|
+
def draw_node(name, options)
|
240
|
+
graph.add_nodes escape_name(name), options
|
241
|
+
end
|
242
|
+
|
243
|
+
def draw_edge(from, to, options)
|
244
|
+
graph.add_edges graph.get_node(escape_name(from)), graph.get_node(escape_name(to)), options if node_exists?(from) and node_exists?(to)
|
245
|
+
end
|
246
|
+
|
247
|
+
def escape_name(name)
|
248
|
+
"m_#{name}"
|
249
|
+
end
|
250
|
+
|
251
|
+
# Returns the title to be used for the graph.
|
252
|
+
def title
|
253
|
+
case options.title
|
254
|
+
when false then nil
|
255
|
+
when true
|
256
|
+
if domain.name then "#{domain.name} domain model" else "Domain model" end
|
257
|
+
else options.title
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# Returns the file name that will be used when saving the diagram.
|
262
|
+
def filename
|
263
|
+
"#{options.filename}.#{options.filetype}"
|
264
|
+
end
|
265
|
+
|
266
|
+
# Returns the default file extension to be used when saving the diagram.
|
267
|
+
def filetype
|
268
|
+
if options.filetype.to_sym == :dot then :none else options.filetype.to_sym end
|
269
|
+
end
|
270
|
+
|
271
|
+
def entity_options(entity, attributes)
|
272
|
+
label = options[:markup] ? "<#{read_template(:html).result(binding)}>" : "#{read_template(:record).result(binding)}"
|
273
|
+
entity_style(entity, attributes).merge :label => label
|
274
|
+
end
|
275
|
+
|
276
|
+
def relationship_options(relationship)
|
277
|
+
relationship_style(relationship).tap do |options|
|
278
|
+
# Edges with a higher weight are optimized to be shorter and straighter.
|
279
|
+
options[:weight] = relationship.strength
|
280
|
+
|
281
|
+
# Indirect relationships should not influence node ranks.
|
282
|
+
options[:constraint] = false if relationship.indirect?
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def specialization_options(specialization)
|
287
|
+
specialization_style(specialization)
|
288
|
+
end
|
289
|
+
|
290
|
+
def read_template(type)
|
291
|
+
ERB.new(File.read(File.expand_path("templates/#{NODE_LABEL_TEMPLATES[type]}", File.dirname(__FILE__))), nil, "<>")
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<% if options.orientation == :vertical %>{<% end %>
|
2
|
+
<table border="0" align="center" cellspacing="0.5" cellpadding="0" width="<%= NODE_WIDTH + 4 %>">
|
3
|
+
<tr><td align="center" valign="bottom" width="<%= NODE_WIDTH %>"><font face="<%= FONTS[:bold] %>" point-size="11"><%= entity.name %></font></td></tr>
|
4
|
+
</table>
|
5
|
+
<% if attributes.any? %>
|
6
|
+
|
|
7
|
+
<table border="0" align="left" cellspacing="2" cellpadding="0" width="<%= NODE_WIDTH + 4 %>">
|
8
|
+
<% attributes.each do |attribute| %>
|
9
|
+
<tr><td align="left" width="<%= NODE_WIDTH %>" port="<%= attribute %>"><%= attribute %> <font face="<%= FONTS[:italic] %>" color="grey60"><%= attribute.type_description %></font></td></tr>
|
10
|
+
<% end %>
|
11
|
+
</table>
|
12
|
+
<% else %>
|
13
|
+
<% end %>
|
14
|
+
<% if options.orientation == :vertical %>}<% end %>
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require "rails_erd/domain"
|
2
|
+
|
3
|
+
module RailsERD
|
4
|
+
# This class is an abstract class that will process a domain model and
|
5
|
+
# allows easy creation of diagrams. To implement a new diagram type, derive
|
6
|
+
# from this class and override +process_entity+, +process_relationship+,
|
7
|
+
# and (optionally) +save+.
|
8
|
+
#
|
9
|
+
# As an example, a diagram class that generates code that can be used with
|
10
|
+
# yUML (http://yuml.me) can be as simple as:
|
11
|
+
#
|
12
|
+
# require "rails_erd/diagram"
|
13
|
+
#
|
14
|
+
# class YumlDiagram < RailsERD::Diagram
|
15
|
+
# setup { @edges = [] }
|
16
|
+
#
|
17
|
+
# each_relationship do |relationship|
|
18
|
+
# return if relationship.indirect?
|
19
|
+
#
|
20
|
+
# arrow = case
|
21
|
+
# when relationship.one_to_one? then "1-1>"
|
22
|
+
# when relationship.one_to_many? then "1-*>"
|
23
|
+
# when relationship.many_to_many? then "*-*>"
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# @edges << "[#{relationship.source}] #{arrow} [#{relationship.destination}]"
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# save { @edges * "\n" }
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# Then, to generate the diagram (example based on the domain model of Gemcutter):
|
33
|
+
#
|
34
|
+
# YumlDiagram.create
|
35
|
+
# #=> "[Rubygem] 1-*> [Ownership]
|
36
|
+
# # [Rubygem] 1-*> [Subscription]
|
37
|
+
# # [Rubygem] 1-*> [Version]
|
38
|
+
# # [Rubygem] 1-1> [Linkset]
|
39
|
+
# # [Rubygem] 1-*> [Dependency]
|
40
|
+
# # [Version] 1-*> [Dependency]
|
41
|
+
# # [User] 1-*> [Ownership]
|
42
|
+
# # [User] 1-*> [Subscription]
|
43
|
+
# # [User] 1-*> [WebHook]"
|
44
|
+
#
|
45
|
+
# For another example implementation, see Diagram::Graphviz, which is the
|
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
|
+
# +:content+, +:primary_keys+, +:foreign_keys+, +:timestamps+, or
|
55
|
+
# +:inheritance+.
|
56
|
+
# disconnected:: Set to +false+ to exclude entities that are not connected to other
|
57
|
+
# entities. Defaults to +false+.
|
58
|
+
# indirect:: Set to +false+ to exclude relationships that are indirect.
|
59
|
+
# Indirect relationships are defined in Active Record with
|
60
|
+
# <tt>has_many :through</tt> associations.
|
61
|
+
# inheritance:: Set to +true+ to include specializations, which correspond to
|
62
|
+
# Rails single table inheritance.
|
63
|
+
# polymorphism:: Set to +true+ to include generalizations, which correspond to
|
64
|
+
# Rails polymorphic associations.
|
65
|
+
# warn:: When set to +false+, no warnings are printed to the
|
66
|
+
# command line while processing the domain model. Defaults
|
67
|
+
# to +true+.
|
68
|
+
class Diagram
|
69
|
+
class << self
|
70
|
+
# Generates a new domain model based on all <tt>ActiveRecord::Base</tt>
|
71
|
+
# subclasses, and creates a new diagram. Use the given options for both
|
72
|
+
# the domain generation and the diagram generation.
|
73
|
+
def create(options = {})
|
74
|
+
new(Domain.generate(options), options).create
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
def setup(&block)
|
80
|
+
callbacks[:setup] = block
|
81
|
+
end
|
82
|
+
|
83
|
+
def each_entity(&block)
|
84
|
+
callbacks[:each_entity] = block
|
85
|
+
end
|
86
|
+
|
87
|
+
def each_relationship(&block)
|
88
|
+
callbacks[:each_relationship] = block
|
89
|
+
end
|
90
|
+
|
91
|
+
def each_specialization(&block)
|
92
|
+
callbacks[:each_specialization] = block
|
93
|
+
end
|
94
|
+
|
95
|
+
def save(&block)
|
96
|
+
callbacks[:save] = block
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def callbacks
|
102
|
+
@callbacks ||= Hash.new { proc {} }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# The options that are used to create this diagram.
|
107
|
+
attr_reader :options
|
108
|
+
|
109
|
+
# The domain that this diagram represents.
|
110
|
+
attr_reader :domain
|
111
|
+
|
112
|
+
# Create a new diagram based on the given domain.
|
113
|
+
def initialize(domain, options = {})
|
114
|
+
@domain, @options = domain, RailsERD.options.merge(options)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Generates and saves the diagram, returning the result of +save+.
|
118
|
+
def create
|
119
|
+
generate
|
120
|
+
save
|
121
|
+
end
|
122
|
+
|
123
|
+
# Generates the diagram, but does not save the output. It is called
|
124
|
+
# internally by Diagram#create.
|
125
|
+
def generate
|
126
|
+
instance_eval &callbacks[:setup]
|
127
|
+
|
128
|
+
filtered_entities.each do |entity|
|
129
|
+
instance_exec entity, filtered_attributes(entity), &callbacks[:each_entity]
|
130
|
+
end
|
131
|
+
|
132
|
+
filtered_specializations.each do |specialization|
|
133
|
+
instance_exec specialization, &callbacks[:each_specialization]
|
134
|
+
end
|
135
|
+
|
136
|
+
filtered_relationships.each do |relationship|
|
137
|
+
instance_exec relationship, &callbacks[:each_relationship]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def save
|
142
|
+
instance_eval &callbacks[:save]
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def callbacks
|
148
|
+
@callbacks ||= self.class.send(:callbacks)
|
149
|
+
end
|
150
|
+
|
151
|
+
def filtered_entities
|
152
|
+
@domain.entities.reject { |entity|
|
153
|
+
options.exclude && entity.model && [options.exclude].flatten.map(&:to_sym).include?(entity.name.to_sym) or
|
154
|
+
options.only && entity.model && ![options.only].flatten.map(&:to_sym).include?(entity.name.to_sym) or
|
155
|
+
!options.inheritance && entity.specialized? or
|
156
|
+
!options.polymorphism && entity.generalized? or
|
157
|
+
!options.disconnected && entity.disconnected?
|
158
|
+
}.compact.tap do |entities|
|
159
|
+
raise "No entities found; create your models first!" if entities.empty?
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def filtered_relationships
|
164
|
+
@domain.relationships.reject { |relationship|
|
165
|
+
!options.indirect && relationship.indirect?
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
def filtered_specializations
|
170
|
+
@domain.specializations.reject { |specialization|
|
171
|
+
!options.inheritance && specialization.inheritance? or
|
172
|
+
!options.polymorphism && specialization.polymorphic?
|
173
|
+
}
|
174
|
+
end
|
175
|
+
|
176
|
+
def filtered_attributes(entity)
|
177
|
+
entity.attributes.reject { |attribute|
|
178
|
+
# Select attributes that satisfy the conditions in the :attributes option.
|
179
|
+
!options.attributes or entity.specialized? or
|
180
|
+
[*options.attributes].none? { |type| attribute.send(:"#{type.to_s.chomp('s')}?") }
|
181
|
+
}
|
182
|
+
end
|
183
|
+
|
184
|
+
def warn(message)
|
185
|
+
puts "Warning: #{message}" if options.warn
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
#--
|
4
|
+
module RailsERD
|
5
|
+
class Domain
|
6
|
+
# Describes an entity's attribute. Attributes correspond directly to
|
7
|
+
# database columns.
|
8
|
+
class Attribute
|
9
|
+
TIMESTAMP_NAMES = %w{created_at created_on updated_at updated_on} # @private :nodoc:
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def from_model(domain, model) # @private :nodoc:
|
13
|
+
attributes = model.columns.collect { |column| new(domain, model, column) }
|
14
|
+
attributes.sort! if RailsERD.options[:sort]
|
15
|
+
|
16
|
+
if RailsERD.options[:prepend_primary]
|
17
|
+
attributes = prepend_primary(model, attributes)
|
18
|
+
end
|
19
|
+
|
20
|
+
attributes
|
21
|
+
end
|
22
|
+
|
23
|
+
def prepend_primary(model, attributes)
|
24
|
+
primary_key = ActiveRecord::Base.get_primary_key(model)
|
25
|
+
primary = attributes.index { |column| column.name == primary_key }
|
26
|
+
|
27
|
+
if primary
|
28
|
+
attributes[primary], attributes[0] = attributes[0], attributes[primary]
|
29
|
+
end
|
30
|
+
|
31
|
+
attributes
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
extend Inspectable
|
36
|
+
inspection_attributes :name, :type
|
37
|
+
|
38
|
+
attr_reader :column # @private :nodoc:
|
39
|
+
|
40
|
+
def initialize(domain, model, column) # @private :nodoc:
|
41
|
+
@domain, @model, @column = domain, model, column
|
42
|
+
end
|
43
|
+
|
44
|
+
# The name of the attribute, equal to the column name.
|
45
|
+
def name
|
46
|
+
column.name
|
47
|
+
end
|
48
|
+
|
49
|
+
# The type of the attribute, equal to the Rails migration type. Can be any
|
50
|
+
# of +:string+, +:integer+, +:boolean+, +:text+, etc.
|
51
|
+
def type
|
52
|
+
column.type or column.sql_type.downcase.to_sym
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns +true+ if this attribute is a content column, that is, if it
|
56
|
+
# is not a primary key, foreign key, timestamp, or inheritance column.
|
57
|
+
def content?
|
58
|
+
!primary_key? and !foreign_key? and !timestamp? and !inheritance?
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns +true+ if this attribute is mandatory. Mandatory attributes
|
62
|
+
# either have a presence validation (+validates_presence_of+), or have a
|
63
|
+
# <tt>NOT NULL</tt> database constraint.
|
64
|
+
def mandatory?
|
65
|
+
!column.null or @model.validators_on(name).map(&:kind).include?(:presence)
|
66
|
+
end
|
67
|
+
|
68
|
+
def unique?
|
69
|
+
@model.validators_on(name).map(&:kind).include?(:uniqueness)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns +true+ if this attribute is the primary key of the entity.
|
73
|
+
def primary_key?
|
74
|
+
@model.primary_key.to_s == name.to_s
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns +true+ if this attribute is used as a foreign key for any
|
78
|
+
# relationship.
|
79
|
+
def foreign_key?
|
80
|
+
@domain.relationships_by_entity_name(@model.name).map(&:associations).flatten.map { |associaton|
|
81
|
+
associaton.send(Domain.foreign_key_method_name)
|
82
|
+
}.include?(name)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns +true+ if this attribute is used for single table inheritance.
|
86
|
+
# These attributes are typically named +type+.
|
87
|
+
def inheritance?
|
88
|
+
@model.inheritance_column == name
|
89
|
+
end
|
90
|
+
|
91
|
+
# Method allows false to be set as an attributes option when making custom graphs.
|
92
|
+
# It rejects all attributes when called from Diagram#filtered_attributes method
|
93
|
+
def false?
|
94
|
+
false
|
95
|
+
end
|
96
|
+
|
97
|
+
# Returns +true+ if this attribute is one of the standard 'magic' Rails
|
98
|
+
# timestamp columns, being +created_at+, +updated_at+, +created_on+ or
|
99
|
+
# +updated_on+.
|
100
|
+
def timestamp?
|
101
|
+
TIMESTAMP_NAMES.include? name
|
102
|
+
end
|
103
|
+
|
104
|
+
def <=>(other) # @private :nodoc:
|
105
|
+
name <=> other.name
|
106
|
+
end
|
107
|
+
|
108
|
+
def to_s # @private :nodoc:
|
109
|
+
name
|
110
|
+
end
|
111
|
+
|
112
|
+
# Returns a description of the attribute type. If the attribute has
|
113
|
+
# a non-standard limit or if it is mandatory, this information is included.
|
114
|
+
#
|
115
|
+
# Example output:
|
116
|
+
# <tt>:integer</tt>:: integer
|
117
|
+
# <tt>:string, :limit => 255</tt>:: string
|
118
|
+
# <tt>:string, :limit => 128</tt>:: string (128)
|
119
|
+
# <tt>:decimal, :precision => 5, :scale => 2/tt>:: decimal (5,2)
|
120
|
+
# <tt>:boolean, :null => false</tt>:: boolean *
|
121
|
+
def type_description
|
122
|
+
type.to_s.tap do |desc|
|
123
|
+
desc << " #{limit_description}" if limit_description
|
124
|
+
desc << " ∗" if mandatory? && !primary_key? # Add a hair space + low asterisk (Unicode characters)
|
125
|
+
desc << " U" if unique? && !primary_key? && !foreign_key? # Add U if unique but non-key
|
126
|
+
desc << " PK" if primary_key?
|
127
|
+
desc << " FK" if foreign_key?
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns any non-standard limit for this attribute. If a column has no
|
132
|
+
# limit or uses a default database limit, this method returns +nil+.
|
133
|
+
def limit
|
134
|
+
return if native_type == 'geometry' || native_type == 'geography'
|
135
|
+
return column.limit.to_i if column.limit != native_type[:limit] and column.limit.respond_to?(:to_i)
|
136
|
+
column.precision.to_i if column.precision != native_type[:precision] and column.precision.respond_to?(:to_i)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns any non-standard scale for this attribute (decimal types only).
|
140
|
+
def scale
|
141
|
+
return column.scale.to_i if column.scale != native_type[:scale] and column.scale.respond_to?(:to_i)
|
142
|
+
0 if column.precision
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns a string that describes the limit for this attribute, such as
|
146
|
+
# +(128)+, or +(5,2)+ for decimal types. Returns nil if no non-standard
|
147
|
+
# limit was set.
|
148
|
+
def limit_description # @private :nodoc:
|
149
|
+
return "(#{limit},#{scale})" if limit and scale
|
150
|
+
return "(#{limit})" if limit
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def native_type
|
156
|
+
@model.connection.native_database_types[type] or {}
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|