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