jw-rails-erd 1.4.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +86 -0
  3. data/Rakefile +20 -0
  4. data/bin/erd +4 -0
  5. data/lib/generators/erd/USAGE +4 -0
  6. data/lib/generators/erd/install_generator.rb +14 -0
  7. data/lib/generators/erd/templates/auto_generate_diagram.rake +6 -0
  8. data/lib/rails-erd.rb +1 -0
  9. data/lib/rails_erd/cli.rb +164 -0
  10. data/lib/rails_erd/config.rb +97 -0
  11. data/lib/rails_erd/custom.rb +99 -0
  12. data/lib/rails_erd/diagram/graphviz.rb +295 -0
  13. data/lib/rails_erd/diagram/templates/node.html.erb +14 -0
  14. data/lib/rails_erd/diagram/templates/node.record.erb +4 -0
  15. data/lib/rails_erd/diagram.rb +188 -0
  16. data/lib/rails_erd/domain/attribute.rb +160 -0
  17. data/lib/rails_erd/domain/entity.rb +104 -0
  18. data/lib/rails_erd/domain/relationship/cardinality.rb +118 -0
  19. data/lib/rails_erd/domain/relationship.rb +203 -0
  20. data/lib/rails_erd/domain/specialization.rb +90 -0
  21. data/lib/rails_erd/domain.rb +153 -0
  22. data/lib/rails_erd/railtie.rb +10 -0
  23. data/lib/rails_erd/tasks.rake +58 -0
  24. data/lib/rails_erd/version.rb +4 -0
  25. data/lib/rails_erd.rb +73 -0
  26. data/lib/tasks/auto_generate_diagram.rake +21 -0
  27. data/test/support_files/erdconfig.another_example +3 -0
  28. data/test/support_files/erdconfig.example +19 -0
  29. data/test/support_files/erdconfig.exclude.example +19 -0
  30. data/test/test_helper.rb +160 -0
  31. data/test/unit/attribute_test.rb +316 -0
  32. data/test/unit/cardinality_test.rb +123 -0
  33. data/test/unit/config_test.rb +110 -0
  34. data/test/unit/diagram_test.rb +352 -0
  35. data/test/unit/domain_test.rb +258 -0
  36. data/test/unit/entity_test.rb +252 -0
  37. data/test/unit/graphviz_test.rb +461 -0
  38. data/test/unit/rake_task_test.rb +174 -0
  39. data/test/unit/relationship_test.rb +476 -0
  40. data/test/unit/specialization_test.rb +67 -0
  41. 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,4 @@
1
+ <% if options.orientation == :vertical %>{<% end %><%= entity.name %><% if attributes.any? %>
2
+ |<% attributes.each do |attribute| %><%=
3
+ attribute %> (<%= attribute.type_description %>)
4
+ <% end %><% end %><% 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