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,104 @@
1
+ module RailsERD
2
+ class Domain
3
+ # Entities represent your Active Record models. Entities may be connected
4
+ # to other entities.
5
+ class Entity
6
+ class << self
7
+ def from_models(domain, models) # @private :nodoc:
8
+ (concrete_from_models(domain, models) + abstract_from_models(domain, models)).sort
9
+ end
10
+
11
+ private
12
+
13
+ def concrete_from_models(domain, models)
14
+ models.collect { |model| new(domain, model.name, model) }
15
+ end
16
+
17
+ def abstract_from_models(domain, models)
18
+ models.collect(&:reflect_on_all_associations).flatten.collect { |association|
19
+ association.options[:as].to_s.classify if association.options[:as]
20
+ }.flatten.compact.uniq.collect { |name| new(domain, name) }
21
+ end
22
+ end
23
+
24
+ extend Inspectable
25
+ inspection_attributes :model
26
+
27
+ # The domain in which this entity resides.
28
+ attr_reader :domain
29
+
30
+ # The Active Record model that this entity corresponds to.
31
+ attr_reader :model
32
+
33
+ # The name of this entity. Equal to the class name of the corresponding
34
+ # model (for concrete entities) or given name (for abstract entities).
35
+ attr_reader :name
36
+
37
+ def initialize(domain, name, model = nil) # @private :nodoc:
38
+ @domain, @name, @model = domain, name, model
39
+ end
40
+
41
+ # Returns an array of attributes for this entity.
42
+ def attributes
43
+ @attributes ||= generalized? ? [] : Attribute.from_model(domain, model)
44
+ end
45
+
46
+ # Returns an array of all relationships that this entity has with other
47
+ # entities in the domain model.
48
+ def relationships
49
+ domain.relationships_by_entity_name(name)
50
+ end
51
+
52
+ # Returns +true+ if this entity has any relationships with other models,
53
+ # +false+ otherwise.
54
+ def connected?
55
+ relationships.any?
56
+ end
57
+
58
+ # Returns +true+ if this entity has no relationships with any other models,
59
+ # +false+ otherwise. Opposite of +connected?+.
60
+ def disconnected?
61
+ relationships.none?
62
+ end
63
+
64
+ # Returns +true+ if this entity is a generalization, which does not
65
+ # correspond with a database table. Generalized entities are either
66
+ # models that are defined as +abstract_class+ or they are constructed
67
+ # from polymorphic interfaces. Any +has_one+ or +has_many+ association
68
+ # that defines a polymorphic interface with <tt>:as => :name</tt> will
69
+ # lead to a generalized entity to be created.
70
+ def generalized?
71
+ !model or !!model.abstract_class?
72
+ end
73
+
74
+ # Returns +true+ if this entity descends from another entity, and is
75
+ # represented in the same table as its parent. In Rails this concept is
76
+ # referred to as single-table inheritance. In entity-relationship
77
+ # diagrams it is called specialization.
78
+ def specialized?
79
+ !!model and !model.descends_from_active_record?
80
+ end
81
+
82
+ # Returns +true+ if this entity does not correspond directly with a
83
+ # database table (if and only if the entity is specialized or
84
+ # generalized).
85
+ def virtual?
86
+ generalized? or specialized?
87
+ end
88
+ alias_method :abstract?, :virtual?
89
+
90
+ # Returns all child entities, if this is a generalized entity.
91
+ def children
92
+ @children ||= domain.specializations_by_entity_name(name).map(&:specialized)
93
+ end
94
+
95
+ def to_s # @private :nodoc:
96
+ name
97
+ end
98
+
99
+ def <=>(other) # @private :nodoc:
100
+ self.name <=> other.name
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,118 @@
1
+ module RailsERD
2
+ class Domain
3
+ class Relationship
4
+ class Cardinality
5
+ extend Inspectable
6
+ inspection_attributes :source_range, :destination_range
7
+
8
+ N = Infinity = 1.0/0 # And beyond.
9
+
10
+ CLASSES = {
11
+ [1, 1] => :one_to_one,
12
+ [1, N] => :one_to_many,
13
+ [N, 1] => :many_to_one,
14
+ [N, N] => :many_to_many
15
+ } # @private :nodoc:
16
+
17
+ # Returns a range that indicates the source (left) cardinality.
18
+ attr_reader :source_range
19
+
20
+ # Returns a range that indicates the destination (right) cardinality.
21
+ attr_reader :destination_range
22
+
23
+ # Create a new cardinality based on a source range and a destination
24
+ # range. These ranges describe which number of values are valid.
25
+ def initialize(source_range, destination_range) # @private :nodoc:
26
+ @source_range = compose_range(source_range)
27
+ @destination_range = compose_range(destination_range)
28
+ end
29
+
30
+ # Returns the name of this cardinality, based on its two cardinal
31
+ # numbers (for source and destination). Can be any of
32
+ # +:one_to_one:+, +:one_to_many+, or +:many_to_many+. The name
33
+ # +:many_to_one+ also exists, but Rails ERD always normalises these
34
+ # kinds of relationships by inverting them, so they become
35
+ # +:one_to_many+ associations.
36
+ #
37
+ # You can also call the equivalent method with a question mark, which
38
+ # will return true if the name corresponds to that method. For example:
39
+ #
40
+ # cardinality.one_to_one?
41
+ # #=> true
42
+ # cardinality.one_to_many?
43
+ # #=> false
44
+ def name
45
+ CLASSES[cardinality_class]
46
+ end
47
+
48
+ # Returns +true+ if the source (left side) is not mandatory.
49
+ def source_optional?
50
+ source_range.first < 1
51
+ end
52
+
53
+ # Returns +true+ if the destination (right side) is not mandatory.
54
+ def destination_optional?
55
+ destination_range.first < 1
56
+ end
57
+
58
+ # Returns the inverse cardinality. Destination becomes source, source
59
+ # becomes destination.
60
+ def inverse
61
+ self.class.new destination_range, source_range
62
+ end
63
+
64
+ CLASSES.each do |cardinality_class, name|
65
+ class_eval <<-RUBY
66
+ def #{name}?
67
+ cardinality_class == #{cardinality_class.inspect}
68
+ end
69
+ RUBY
70
+ end
71
+
72
+ def ==(other) # @private :nodoc:
73
+ source_range == other.source_range and destination_range == other.destination_range
74
+ end
75
+
76
+ def <=>(other) # @private :nodoc:
77
+ (cardinality_class <=> other.cardinality_class).nonzero? or
78
+ compare_with(other) { |x| x.source_range.first + x.destination_range.first }.nonzero? or
79
+ compare_with(other) { |x| x.source_range.last + x.destination_range.last }.nonzero? or
80
+ compare_with(other) { |x| x.source_range.last }.nonzero? or
81
+ compare_with(other) { |x| x.destination_range.last }
82
+ end
83
+
84
+ # Returns an array with the cardinality classes for the source and
85
+ # destination of this cardinality. Possible return values are:
86
+ # <tt>[1, 1]</tt>, <tt>[1, N]</tt>, <tt>[N, N]</tt>, and (in theory)
87
+ # <tt>[N, 1]</tt>.
88
+ def cardinality_class
89
+ [source_cardinality_class, destination_cardinality_class]
90
+ end
91
+
92
+ protected
93
+
94
+ # The cardinality class of the source (left side). Either +1+ or +Infinity+.
95
+ def source_cardinality_class
96
+ source_range.last == 1 ? 1 : N
97
+ end
98
+
99
+ # The cardinality class of the destination (right side). Either +1+ or +Infinity+.
100
+ def destination_cardinality_class
101
+ destination_range.last == 1 ? 1 : N
102
+ end
103
+
104
+ private
105
+
106
+ def compose_range(r)
107
+ return r..r if r.kind_of?(Integer) && r > 0
108
+ return (r.begin)..(r.end - 1) if r.exclude_end?
109
+ r
110
+ end
111
+
112
+ def compare_with(other, &block)
113
+ yield(self) <=> yield(other)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,203 @@
1
+ require "set"
2
+ require "active_support/core_ext/module/delegation"
3
+ require "rails_erd/domain/relationship/cardinality"
4
+
5
+ module RailsERD
6
+ class Domain
7
+ # Describes a relationship between two entities. A relationship is detected
8
+ # based on Active Record associations. One relationship may represent more
9
+ # than one association, however. Related associations are grouped together.
10
+ # Associations are related if they share the same foreign key, or the same
11
+ # join table in the case of many-to-many associations.
12
+ class Relationship
13
+ N = Cardinality::N
14
+
15
+ class << self
16
+ def from_associations(domain, associations) # @private :nodoc:
17
+ assoc_groups = associations.group_by { |assoc| association_identity(assoc) }
18
+ assoc_groups.collect { |_, assoc_group| new(domain, assoc_group.to_a) }
19
+ end
20
+
21
+ private
22
+
23
+ def association_identity(association)
24
+ identifier = association_identifier(association)
25
+ Set[identifier, association_owner(association), association_target(association)]
26
+ end
27
+
28
+ def association_identifier(association)
29
+ if association.macro == :has_and_belongs_to_many
30
+ # Rails 4+ supports the join_table method, and doesn't expose it
31
+ # as an option if it's an implicit default.
32
+ (association.respond_to?(:join_table) && association.join_table) || association.options[:join_table]
33
+ else
34
+ association.options[:through] || association.send(Domain.foreign_key_method_name).to_s
35
+ end
36
+ end
37
+
38
+ def association_owner(association)
39
+ association.options[:as] ? association.options[:as].to_s.classify : association.active_record.name
40
+ end
41
+
42
+ def association_target(association)
43
+ association.options[:polymorphic] ? association.class_name : association.klass.name
44
+ end
45
+ end
46
+
47
+ extend Inspectable
48
+ inspection_attributes :source, :destination
49
+
50
+ # The domain in which this relationship is defined.
51
+ attr_reader :domain
52
+
53
+ # The source entity. It corresponds to the model that has defined a
54
+ # +has_one+ or +has_many+ association with the other model.
55
+ attr_reader :source
56
+
57
+ # The destination entity. It corresponds to the model that has defined
58
+ # a +belongs_to+ association with the other model.
59
+ attr_reader :destination
60
+
61
+ delegate :one_to_one?, :one_to_many?, :many_to_many?, :source_optional?,
62
+ :destination_optional?, :to => :cardinality
63
+
64
+ def initialize(domain, associations) # @private :nodoc:
65
+ @domain = domain
66
+ @reverse_associations, @forward_associations = partition_associations(associations)
67
+
68
+ assoc = @forward_associations.first || @reverse_associations.first
69
+ @source = @domain.entity_by_name(self.class.send(:association_owner, assoc))
70
+ @destination = @domain.entity_by_name(self.class.send(:association_target, assoc))
71
+ @source, @destination = @destination, @source if assoc.belongs_to?
72
+ end
73
+
74
+ # Returns all Active Record association objects that describe this
75
+ # relationship.
76
+ def associations
77
+ @forward_associations + @reverse_associations
78
+ end
79
+
80
+ # Returns the cardinality of this relationship.
81
+ def cardinality
82
+ @cardinality ||= begin
83
+ reverse_max = any_habtm?(associations) ? N : 1
84
+ forward_range = associations_range(@forward_associations, N)
85
+ reverse_range = associations_range(@reverse_associations, reverse_max)
86
+ Cardinality.new(reverse_range, forward_range)
87
+ end
88
+ end
89
+
90
+ # Indicates if a relationship is indirect, that is, if it is defined
91
+ # through other relationships. Indirect relationships are created in
92
+ # Rails with <tt>has_many :through</tt> or <tt>has_one :through</tt>
93
+ # association macros.
94
+ def indirect?
95
+ !@forward_associations.empty? and @forward_associations.all?(&:through_reflection)
96
+ end
97
+
98
+ # Indicates whether or not the relationship is defined by two inverse
99
+ # associations (e.g. a +has_many+ and a corresponding +belongs_to+
100
+ # association).
101
+ def mutual?
102
+ @forward_associations.any? and @reverse_associations.any?
103
+ end
104
+
105
+ # Indicates whether or not this relationship connects an entity with itself.
106
+ def recursive?
107
+ @source == @destination
108
+ end
109
+
110
+ # Indicates whether the destination cardinality class of this relationship
111
+ # is equal to one. This is +true+ for one-to-one relationships only.
112
+ def to_one?
113
+ cardinality.cardinality_class[1] == 1
114
+ end
115
+
116
+ # Indicates whether the destination cardinality class of this relationship
117
+ # is equal to infinity. This is +true+ for one-to-many or
118
+ # many-to-many relationships only.
119
+ def to_many?
120
+ cardinality.cardinality_class[1] != 1
121
+ end
122
+
123
+ # Indicates whether the source cardinality class of this relationship
124
+ # is equal to one. This is +true+ for one-to-one or
125
+ # one-to-many relationships only.
126
+ def one_to?
127
+ cardinality.cardinality_class[0] == 1
128
+ end
129
+
130
+ # Indicates whether the source cardinality class of this relationship
131
+ # is equal to infinity. This is +true+ for many-to-many relationships only.
132
+ def many_to?
133
+ cardinality.cardinality_class[0] != 1
134
+ end
135
+
136
+ # The strength of a relationship is equal to the number of associations
137
+ # that describe it.
138
+ def strength
139
+ if source.generalized? then 1 else associations.size end
140
+ end
141
+
142
+ def <=>(other) # @private :nodoc:
143
+ (source.name <=> other.source.name).nonzero? or (destination.name <=> other.destination.name)
144
+ end
145
+
146
+ private
147
+
148
+ def partition_associations(associations)
149
+ if any_habtm?(associations)
150
+ # Many-to-many associations don't have a clearly defined direction.
151
+ # We sort by name and use the first model as the source.
152
+ source = associations.map(&:active_record).sort_by(&:name).first
153
+ associations.partition { |association| association.active_record != source }
154
+ else
155
+ associations.partition(&:belongs_to?)
156
+ end
157
+ end
158
+
159
+ def associations_range(associations, absolute_max)
160
+ # The minimum of the range is the maximum value of each association
161
+ # minimum. If there is none, it is zero by definition. The reasoning is
162
+ # that from all associations, if only one has a required minimum, then
163
+ # this side of the relationship has a cardinality of at least one.
164
+ min = associations.map { |assoc| association_minimum(assoc) }.max || 0
165
+
166
+ # The maximum of the range is the maximum value of each association
167
+ # maximum. If there is none, it is equal to the absolute maximum. If
168
+ # only one association has a high cardinality on this side, the
169
+ # relationship itself has the same maximum cardinality.
170
+ max = associations.map { |assoc| association_maximum(assoc) }.max || absolute_max
171
+
172
+ min..max
173
+ end
174
+
175
+ def association_minimum(association)
176
+ minimum = association_validators(:presence, association).any? ||
177
+ foreign_key_required?(association) ? 1 : 0
178
+ length_validators = association_validators(:length, association)
179
+ length_validators.map { |v| v.options[:minimum] }.compact.max or minimum
180
+ end
181
+
182
+ def association_maximum(association)
183
+ maximum = association.collection? ? N : 1
184
+ length_validators = association_validators(:length, association)
185
+ length_validators.map { |v| v.options[:maximum] }.compact.min or maximum
186
+ end
187
+
188
+ def association_validators(kind, association)
189
+ association.active_record.validators_on(association.name).select { |v| v.kind == kind }
190
+ end
191
+
192
+ def any_habtm?(associations)
193
+ associations.any? { |association| association.macro == :has_and_belongs_to_many }
194
+ end
195
+
196
+ def foreign_key_required?(association)
197
+ if !association.active_record.abstract_class? and association.belongs_to?
198
+ column = association.active_record.columns_hash[association.send(Domain.foreign_key_method_name)] and !column.null
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,90 @@
1
+ module RailsERD
2
+ class Domain
3
+ # Describes the specialization of an entity. Specialized entities correspond
4
+ # to inheritance or polymorphism. In Rails, specialization is referred to
5
+ # as single table inheritance, while generalization is referred to as
6
+ # polymorphism or abstract classes.
7
+ class Specialization
8
+ class << self
9
+ def from_models(domain, models) # @private :nodoc:
10
+ models = polymorphic_from_models(domain, models) +
11
+ inheritance_from_models(domain, models) +
12
+ abstract_from_models(domain, models)
13
+ models.sort
14
+ end
15
+
16
+ private
17
+
18
+ def polymorphic_from_models(domain, models)
19
+ models.collect(&:reflect_on_all_associations).flatten.collect { |association|
20
+ [association.options[:as].to_s.classify, association.active_record.name] if association.options[:as]
21
+ }.compact.uniq.collect { |names|
22
+ new(domain, domain.entity_by_name(names.first), domain.entity_by_name(names.last))
23
+ }
24
+ end
25
+
26
+ def inheritance_from_models(domain, models)
27
+ models.reject(&:descends_from_active_record?).collect { |model|
28
+ new(domain, domain.entity_by_name(model.base_class.name), domain.entity_by_name(model.name))
29
+ }
30
+ end
31
+
32
+ def abstract_from_models(domain, models)
33
+ models.select(&:abstract_class?).collect(&:descendants).flatten.collect { |model|
34
+ new(domain, domain.entity_by_name(model.superclass.name), domain.entity_by_name(model.name))
35
+ }
36
+ end
37
+ end
38
+
39
+ extend Inspectable
40
+ inspection_attributes :generalized, :specialized
41
+
42
+ # The domain in which this specialization is defined.
43
+ attr_reader :domain
44
+
45
+ # The source entity.
46
+ attr_reader :generalized
47
+
48
+ # The destination entity.
49
+ attr_reader :specialized
50
+
51
+ def initialize(domain, generalized, specialized) # @private :nodoc:
52
+ @domain = domain
53
+ @generalized = generalized || NullGeneralized.new
54
+ @specialized = specialized || NullSpecialization.new
55
+ end
56
+
57
+ def generalization?
58
+ generalized.generalized?
59
+ end
60
+ alias_method :polymorphic?, :generalization?
61
+
62
+ def specialization?
63
+ !generalization?
64
+ end
65
+ alias_method :inheritance?, :specialization?
66
+
67
+ def <=>(other) # @private :nodoc:
68
+ (generalized.name <=> other.generalized.name).nonzero? or (specialized.name <=> other.specialized.name)
69
+ end
70
+ end
71
+
72
+ class NullSpecialization
73
+ def name
74
+ ""
75
+ end
76
+ def generalized?
77
+ false
78
+ end
79
+ end
80
+
81
+ class NullGeneralized
82
+ def name
83
+ ""
84
+ end
85
+ def generalized?
86
+ true
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,153 @@
1
+ require "rails_erd"
2
+ require "rails_erd/domain/attribute"
3
+ require "rails_erd/domain/entity"
4
+ require "rails_erd/domain/relationship"
5
+ require "rails_erd/domain/specialization"
6
+
7
+ module RailsERD
8
+ # The domain describes your Rails domain model. This class is the starting
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+.
18
+ class Domain
19
+ class << self
20
+ # Generates a domain model object based on all loaded subclasses of
21
+ # <tt>ActiveRecord::Base</tt>. Make sure your models are loaded before calling
22
+ # this method.
23
+ #
24
+ # The +options+ hash allows you to override the default options. For a
25
+ # list of available options, see RailsERD.
26
+ def generate(options = {})
27
+ new ActiveRecord::Base.descendants, options
28
+ end
29
+
30
+ # Returns the method name to retrieve the foreign key from an
31
+ # association reflection object.
32
+ def foreign_key_method_name # @private :nodoc:
33
+ @foreign_key_method_name ||= ActiveRecord::Reflection::AssociationReflection.method_defined?(:foreign_key) ? :foreign_key : :primary_key_name
34
+ end
35
+ end
36
+
37
+ extend Inspectable
38
+ inspection_attributes
39
+
40
+ # The options that are used to generate this domain model.
41
+ attr_reader :options
42
+
43
+ # Create a new domain model object based on the given array of models.
44
+ # The given models are assumed to be subclasses of <tt>ActiveRecord::Base</tt>.
45
+ def initialize(models = [], options = {})
46
+ @source_models, @options = models, RailsERD.options.merge(options)
47
+ end
48
+
49
+ # Returns the domain model name, which is the name of your Rails
50
+ # application or +nil+ outside of Rails.
51
+ def name
52
+ defined? Rails and Rails.application and Rails.application.class.parent.name
53
+ end
54
+
55
+ # Returns all entities of your domain model.
56
+ def entities
57
+ @entities ||= Entity.from_models(self, models)
58
+ end
59
+
60
+ # Returns all relationships in your domain model.
61
+ def relationships
62
+ @relationships ||= Relationship.from_associations(self, associations)
63
+ end
64
+
65
+ # Returns all specializations in your domain model.
66
+ def specializations
67
+ @specializations ||= Specialization.from_models(self, models)
68
+ end
69
+
70
+ # Returns a specific entity object for the given Active Record model.
71
+ def entity_by_name(name) # @private :nodoc:
72
+ entity_mapping[name]
73
+ end
74
+
75
+ # Returns an array of relationships for the given Active Record model.
76
+ def relationships_by_entity_name(name) # @private :nodoc:
77
+ relationships_mapping[name] or []
78
+ end
79
+
80
+ def specializations_by_entity_name(name)
81
+ specializations_mapping[name] or []
82
+ end
83
+
84
+ def warn(message) # @private :nodoc:
85
+ puts "Warning: #{message}" if options.warn
86
+ end
87
+
88
+ private
89
+
90
+ def entity_mapping
91
+ @entity_mapping ||= {}.tap do |mapping|
92
+ entities.each do |entity|
93
+ mapping[entity.name] = entity
94
+ end
95
+ end
96
+ end
97
+
98
+ def relationships_mapping
99
+ @relationships_mapping ||= {}.tap do |mapping|
100
+ relationships.each do |relationship|
101
+ (mapping[relationship.source.name] ||= []) << relationship
102
+ (mapping[relationship.destination.name] ||= []) << relationship
103
+ end
104
+ end
105
+ end
106
+
107
+ def specializations_mapping
108
+ @specializations_mapping ||= {}.tap do |mapping|
109
+ specializations.each do |specialization|
110
+ (mapping[specialization.generalized.name] ||= []) << specialization
111
+ (mapping[specialization.specialized.name] ||= []) << specialization
112
+ end
113
+ end
114
+ end
115
+
116
+ def models
117
+ @models ||= @source_models.select { |model| check_model_validity(model) }.reject { |model| check_habtm_model(model) }
118
+ end
119
+
120
+ def associations
121
+ @associations ||= models.collect(&:reflect_on_all_associations).flatten.select { |assoc| check_association_validity(assoc) }
122
+ end
123
+
124
+ def check_model_validity(model)
125
+ model.abstract_class? or model.table_exists? or raise "table #{model.table_name} does not exist"
126
+ rescue => e
127
+ warn "Ignoring invalid model #{model.name} (#{e.message})"
128
+ end
129
+
130
+ def check_association_validity(association)
131
+ # Raises an ActiveRecord::ActiveRecordError if the association is broken.
132
+ association.check_validity!
133
+
134
+ if association.options[:polymorphic]
135
+ entity_name = association.class_name
136
+ entity_by_name(entity_name) or raise "polymorphic interface #{entity_name} does not exist"
137
+ else
138
+ entity_name = association.klass.name # Raises NameError if the associated class cannot be found.
139
+ entity_by_name(entity_name) or raise "model #{entity_name} exists, but is not included in domain"
140
+ end
141
+ rescue => e
142
+ warn "Ignoring invalid association #{association_description(association)} (#{e.message})"
143
+ end
144
+
145
+ def association_description(association)
146
+ "#{association.name.inspect} on #{association.active_record}"
147
+ end
148
+
149
+ def check_habtm_model(model)
150
+ model.name.start_with?("HABTM_")
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,10 @@
1
+ module RailsERD
2
+ # Rails ERD integrates with Rails 3. If you add it to your +Gemfile+, you
3
+ # will gain a Rake task called +erd+, which you can use to generate diagrams
4
+ # of your domain model.
5
+ class Railtie < Rails::Railtie
6
+ rake_tasks do
7
+ load "rails_erd/tasks.rake"
8
+ end
9
+ end
10
+ end