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