rails-erd 0.4.5 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.rdoc +18 -0
- data/LICENSE +1 -1
- data/README.md +3 -3
- data/Rakefile +57 -46
- data/bin/erd +4 -0
- data/lib/rails_erd.rb +4 -2
- data/lib/rails_erd/cli.rb +149 -0
- data/lib/rails_erd/diagram.rb +18 -16
- data/lib/rails_erd/diagram/graphviz.rb +23 -2
- data/lib/rails_erd/domain/attribute.rb +9 -3
- data/lib/rails_erd/domain/entity.rb +15 -15
- data/lib/rails_erd/domain/relationship.rb +11 -1
- data/lib/rails_erd/domain/relationship/cardinality.rb +15 -15
- data/lib/rails_erd/domain/specialization.rb +8 -8
- data/lib/rails_erd/version.rb +4 -0
- data/test/test_helper.rb +14 -11
- data/test/unit/attribute_test.rb +66 -3
- data/test/unit/cardinality_test.rb +10 -10
- data/test/unit/diagram_test.rb +44 -18
- data/test/unit/domain_test.rb +20 -20
- data/test/unit/entity_test.rb +19 -19
- data/test/unit/graphviz_test.rb +53 -10
- data/test/unit/rake_task_test.rb +8 -8
- data/test/unit/relationship_test.rb +31 -31
- data/test/unit/specialization_test.rb +3 -3
- metadata +72 -108
- data/.gemtest +0 -0
- data/Gemfile +0 -19
- data/Gemfile.lock +0 -44
- data/VERSION +0 -1
- data/rails-erd.gemspec +0 -105
@@ -103,6 +103,26 @@ module RailsERD
|
|
103
103
|
end
|
104
104
|
end
|
105
105
|
|
106
|
+
module Crowsfoot
|
107
|
+
include Simple
|
108
|
+
def relationship_style(relationship)
|
109
|
+
{}.tap do |options|
|
110
|
+
options[:style] = :dotted if relationship.indirect?
|
111
|
+
|
112
|
+
# Cardinality is "look-across".
|
113
|
+
dst = relationship.to_many? ? "crow" : "tee"
|
114
|
+
src = relationship.many_to? ? "crow" : "tee"
|
115
|
+
|
116
|
+
# Participation is "look-across".
|
117
|
+
dst << (relationship.destination_optional? ? "odot" : "tee")
|
118
|
+
src << (relationship.source_optional? ? "odot" : "tee")
|
119
|
+
|
120
|
+
options[:arrowsize] = 0.6
|
121
|
+
options[:arrowhead], options[:arrowtail] = dst, src
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
106
126
|
module Bachman
|
107
127
|
include Simple
|
108
128
|
def relationship_style(relationship)
|
@@ -116,6 +136,7 @@ module RailsERD
|
|
116
136
|
# Cardinality is "look-across".
|
117
137
|
dst << "normal" if relationship.to_many?
|
118
138
|
src << "normal" if relationship.many_to?
|
139
|
+
|
119
140
|
options[:arrowsize] = 0.6
|
120
141
|
options[:arrowhead], options[:arrowtail] = dst, src
|
121
142
|
end
|
@@ -204,11 +225,11 @@ module RailsERD
|
|
204
225
|
end
|
205
226
|
|
206
227
|
def draw_node(name, options)
|
207
|
-
graph.
|
228
|
+
graph.add_nodes escape_name(name), options
|
208
229
|
end
|
209
230
|
|
210
231
|
def draw_edge(from, to, options)
|
211
|
-
graph.
|
232
|
+
graph.add_edges graph.get_node(escape_name(from)), graph.get_node(escape_name(to)), options if node_exists?(from) and node_exists?(to)
|
212
233
|
end
|
213
234
|
|
214
235
|
def escape_name(name)
|
@@ -31,7 +31,7 @@ module RailsERD
|
|
31
31
|
# The type of the attribute, equal to the Rails migration type. Can be any
|
32
32
|
# of +:string+, +:integer+, +:boolean+, +:text+, etc.
|
33
33
|
def type
|
34
|
-
column.type
|
34
|
+
column.type or column.sql_type.downcase.to_sym
|
35
35
|
end
|
36
36
|
|
37
37
|
# Returns +true+ if this attribute is a content column, that is, if it
|
@@ -100,12 +100,12 @@ module RailsERD
|
|
100
100
|
# Returns any non-standard limit for this attribute. If a column has no
|
101
101
|
# limit or uses a default database limit, this method returns +nil+.
|
102
102
|
def limit
|
103
|
-
column.limit if column.limit !=
|
103
|
+
column.limit.to_i if column.limit != native_type[:limit] and column.limit.respond_to?(:to_i)
|
104
104
|
end
|
105
105
|
|
106
106
|
# Returns any non-standard scale for this attribute (decimal types only).
|
107
107
|
def scale
|
108
|
-
column.scale if column.scale !=
|
108
|
+
column.scale.to_i if column.scale != native_type[:scale] and column.scale.respond_to?(:to_i)
|
109
109
|
end
|
110
110
|
|
111
111
|
# Returns a string that describes the limit for this attribute, such as
|
@@ -115,6 +115,12 @@ module RailsERD
|
|
115
115
|
return "(#{limit},#{scale})" if limit and scale
|
116
116
|
return "(#{limit})" if limit
|
117
117
|
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def native_type
|
122
|
+
@model.connection.native_database_types[type] or {}
|
123
|
+
end
|
118
124
|
end
|
119
125
|
end
|
120
126
|
end
|
@@ -7,29 +7,29 @@ module RailsERD
|
|
7
7
|
def from_models(domain, models) # @private :nodoc:
|
8
8
|
(concrete_from_models(domain, models) + abstract_from_models(domain, models)).sort
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
private
|
12
|
-
|
12
|
+
|
13
13
|
def concrete_from_models(domain, models)
|
14
14
|
models.collect { |model| new(domain, model.name, model) }
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
def abstract_from_models(domain, models)
|
18
18
|
models.collect(&:reflect_on_all_associations).flatten.collect { |association|
|
19
19
|
association.options[:as].to_s.classify if association.options[:as]
|
20
20
|
}.flatten.compact.uniq.collect { |name| new(domain, name) }
|
21
21
|
end
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
extend Inspectable
|
25
25
|
inspection_attributes :model
|
26
|
-
|
26
|
+
|
27
27
|
# The domain in which this entity resides.
|
28
28
|
attr_reader :domain
|
29
|
-
|
29
|
+
|
30
30
|
# The Active Record model that this entity corresponds to.
|
31
31
|
attr_reader :model
|
32
|
-
|
32
|
+
|
33
33
|
# The name of this entity. Equal to the class name of the corersponding
|
34
34
|
# model (for concrete entities) or given name (for abstract entities).
|
35
35
|
attr_reader :name
|
@@ -37,12 +37,12 @@ module RailsERD
|
|
37
37
|
def initialize(domain, name, model = nil) # @private :nodoc:
|
38
38
|
@domain, @name, @model = domain, name, model
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
41
|
# Returns an array of attributes for this entity.
|
42
42
|
def attributes
|
43
43
|
@attributes ||= if generalized? then [] else Attribute.from_model(domain, model) end
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
46
|
# Returns an array of all relationships that this entity has with other
|
47
47
|
# entities in the domain model.
|
48
48
|
def relationships
|
@@ -60,7 +60,7 @@ module RailsERD
|
|
60
60
|
def disconnected?
|
61
61
|
relationships.none?
|
62
62
|
end
|
63
|
-
|
63
|
+
|
64
64
|
# Returns +true+ if this entity is a generalization, which does not
|
65
65
|
# correspond with a database table. Generalized entities are constructed
|
66
66
|
# from polymorphic interfaces. Any +has_one+ or +has_many+ association
|
@@ -69,7 +69,7 @@ module RailsERD
|
|
69
69
|
def generalized?
|
70
70
|
!model
|
71
71
|
end
|
72
|
-
|
72
|
+
|
73
73
|
# Returns +true+ if this entity descends from another entity, and is
|
74
74
|
# represented in the same table as its parent. In Rails this concept is
|
75
75
|
# referred to as single-table inheritance. In entity-relationship
|
@@ -77,23 +77,23 @@ module RailsERD
|
|
77
77
|
def specialized?
|
78
78
|
!generalized? and !model.descends_from_active_record?
|
79
79
|
end
|
80
|
-
|
80
|
+
|
81
81
|
# Returns +true+ if this entity does not correspond directly with a
|
82
82
|
# database table (if and only if the entity is specialized or
|
83
83
|
# generalized).
|
84
84
|
def abstract?
|
85
85
|
specialized? or generalized?
|
86
86
|
end
|
87
|
-
|
87
|
+
|
88
88
|
# Returns all child entities, if this is a generalized entity.
|
89
89
|
def children
|
90
90
|
@children ||= domain.specializations_by_entity_name(name).map(&:specialized)
|
91
91
|
end
|
92
|
-
|
92
|
+
|
93
93
|
def to_s # @private :nodoc:
|
94
94
|
name
|
95
95
|
end
|
96
|
-
|
96
|
+
|
97
97
|
def <=>(other) # @private :nodoc:
|
98
98
|
self.name <=> other.name
|
99
99
|
end
|
@@ -21,10 +21,20 @@ module RailsERD
|
|
21
21
|
private
|
22
22
|
|
23
23
|
def association_identity(association)
|
24
|
-
identifier = association
|
24
|
+
identifier = association_identifier(association)
|
25
25
|
Set[identifier, association_owner(association), association_target(association)]
|
26
26
|
end
|
27
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
|
+
|
28
38
|
def association_owner(association)
|
29
39
|
association.options[:as] ? association.options[:as].to_s.classify : association.active_record.name
|
30
40
|
end
|
@@ -4,9 +4,9 @@ module RailsERD
|
|
4
4
|
class Cardinality
|
5
5
|
extend Inspectable
|
6
6
|
inspection_attributes :source_range, :destination_range
|
7
|
-
|
7
|
+
|
8
8
|
N = Infinity = 1.0/0 # And beyond.
|
9
|
-
|
9
|
+
|
10
10
|
CLASSES = {
|
11
11
|
[1, 1] => :one_to_one,
|
12
12
|
[1, N] => :one_to_many,
|
@@ -16,7 +16,7 @@ module RailsERD
|
|
16
16
|
|
17
17
|
# Returns a range that indicates the source (left) cardinality.
|
18
18
|
attr_reader :source_range
|
19
|
-
|
19
|
+
|
20
20
|
# Returns a range that indicates the destination (right) cardinality.
|
21
21
|
attr_reader :destination_range
|
22
22
|
|
@@ -26,7 +26,7 @@ module RailsERD
|
|
26
26
|
@source_range = compose_range(source_range)
|
27
27
|
@destination_range = compose_range(destination_range)
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
30
|
# Returns the name of this cardinality, based on its two cardinal
|
31
31
|
# numbers (for source and destination). Can be any of
|
32
32
|
# +:one_to_one:+, +:one_to_many+, or +:many_to_many+. The name
|
@@ -44,23 +44,23 @@ module RailsERD
|
|
44
44
|
def name
|
45
45
|
CLASSES[cardinality_class]
|
46
46
|
end
|
47
|
-
|
47
|
+
|
48
48
|
# Returns +true+ if the source (left side) is not mandatory.
|
49
49
|
def source_optional?
|
50
50
|
source_range.first < 1
|
51
51
|
end
|
52
|
-
|
52
|
+
|
53
53
|
# Returns +true+ if the destination (right side) is not mandatory.
|
54
54
|
def destination_optional?
|
55
55
|
destination_range.first < 1
|
56
56
|
end
|
57
|
-
|
57
|
+
|
58
58
|
# Returns the inverse cardinality. Destination becomes source, source
|
59
59
|
# becomes destination.
|
60
60
|
def inverse
|
61
61
|
self.class.new destination_range, source_range
|
62
62
|
end
|
63
|
-
|
63
|
+
|
64
64
|
CLASSES.each do |cardinality_class, name|
|
65
65
|
class_eval <<-RUBY
|
66
66
|
def #{name}?
|
@@ -68,11 +68,11 @@ module RailsERD
|
|
68
68
|
end
|
69
69
|
RUBY
|
70
70
|
end
|
71
|
-
|
71
|
+
|
72
72
|
def ==(other) # @private :nodoc:
|
73
73
|
source_range == other.source_range and destination_range == other.destination_range
|
74
74
|
end
|
75
|
-
|
75
|
+
|
76
76
|
def <=>(other) # @private :nodoc:
|
77
77
|
(cardinality_class <=> other.cardinality_class).nonzero? or
|
78
78
|
compare_with(other) { |x| x.source_range.first + x.destination_range.first }.nonzero? or
|
@@ -80,7 +80,7 @@ module RailsERD
|
|
80
80
|
compare_with(other) { |x| x.source_range.last }.nonzero? or
|
81
81
|
compare_with(other) { |x| x.destination_range.last }
|
82
82
|
end
|
83
|
-
|
83
|
+
|
84
84
|
# Returns an array with the cardinality classes for the source and
|
85
85
|
# destination of this cardinality. Possible return values are:
|
86
86
|
# <tt>[1, 1]</tt>, <tt>[1, N]</tt>, <tt>[N, N]</tt>, and (in theory)
|
@@ -88,21 +88,21 @@ module RailsERD
|
|
88
88
|
def cardinality_class
|
89
89
|
[source_cardinality_class, destination_cardinality_class]
|
90
90
|
end
|
91
|
-
|
91
|
+
|
92
92
|
protected
|
93
93
|
|
94
94
|
# The cardinality class of the source (left side). Either +1+ or +Infinity+.
|
95
95
|
def source_cardinality_class
|
96
96
|
source_range.last == 1 ? 1 : N
|
97
97
|
end
|
98
|
-
|
98
|
+
|
99
99
|
# The cardinality class of the destination (right side). Either +1+ or +Infinity+.
|
100
100
|
def destination_cardinality_class
|
101
101
|
destination_range.last == 1 ? 1 : N
|
102
102
|
end
|
103
|
-
|
103
|
+
|
104
104
|
private
|
105
|
-
|
105
|
+
|
106
106
|
def compose_range(r)
|
107
107
|
return r..r if r.kind_of?(Integer) && r > 0
|
108
108
|
return (r.begin)..(r.end - 1) if r.exclude_end?
|
@@ -8,9 +8,9 @@ module RailsERD
|
|
8
8
|
def from_models(domain, models) # @private :nodoc:
|
9
9
|
(inheritance_from_models(domain, models) + polymorphic_from_models(domain, models)).sort
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
private
|
13
|
-
|
13
|
+
|
14
14
|
def polymorphic_from_models(domain, models)
|
15
15
|
models.collect(&:reflect_on_all_associations).flatten.collect { |association|
|
16
16
|
[association.options[:as].to_s.classify, association.active_record.name] if association.options[:as]
|
@@ -18,14 +18,14 @@ module RailsERD
|
|
18
18
|
new(domain, domain.entity_by_name(names.first), domain.entity_by_name(names.last))
|
19
19
|
}
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
def inheritance_from_models(domain, models)
|
23
23
|
models.reject(&:descends_from_active_record?).collect { |model|
|
24
24
|
new(domain, domain.entity_by_name(model.base_class.name), domain.entity_by_name(model.name))
|
25
25
|
}
|
26
26
|
end
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
extend Inspectable
|
30
30
|
inspection_attributes :generalized, :specialized
|
31
31
|
|
@@ -34,18 +34,18 @@ module RailsERD
|
|
34
34
|
|
35
35
|
# The source entity.
|
36
36
|
attr_reader :generalized
|
37
|
-
|
37
|
+
|
38
38
|
# The destination entity.
|
39
39
|
attr_reader :specialized
|
40
|
-
|
40
|
+
|
41
41
|
def initialize(domain, generalized, specialized) # @private :nodoc:
|
42
42
|
@domain, @generalized, @specialized = domain, generalized, specialized
|
43
43
|
end
|
44
|
-
|
44
|
+
|
45
45
|
def inheritance?
|
46
46
|
!polymorphic?
|
47
47
|
end
|
48
|
-
|
48
|
+
|
49
49
|
def polymorphic?
|
50
50
|
generalized.generalized?
|
51
51
|
end
|
data/test/test_helper.rb
CHANGED
@@ -14,7 +14,7 @@ class ActiveSupport::TestCase
|
|
14
14
|
|
15
15
|
def create_table(table, columns = {}, pk = nil)
|
16
16
|
opts = if pk then { :primary_key => pk } else { :id => false } end
|
17
|
-
ActiveRecord::Schema.
|
17
|
+
ActiveRecord::Schema.instance_eval do
|
18
18
|
suppress_messages do
|
19
19
|
create_table table, opts do |t|
|
20
20
|
columns.each do |column, type|
|
@@ -23,14 +23,16 @@ class ActiveSupport::TestCase
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
end
|
26
|
+
ActiveRecord::Base.clear_cache!
|
26
27
|
end
|
27
|
-
|
28
|
+
|
28
29
|
def add_column(*args)
|
29
|
-
ActiveRecord::Schema.
|
30
|
+
ActiveRecord::Schema.instance_eval do
|
30
31
|
suppress_messages do
|
31
32
|
add_column *args
|
32
33
|
end
|
33
34
|
end
|
35
|
+
ActiveRecord::Base.clear_cache!
|
34
36
|
end
|
35
37
|
|
36
38
|
def create_model(name, *args, &block)
|
@@ -42,13 +44,13 @@ class ActiveSupport::TestCase
|
|
42
44
|
create_table Object.const_get(name.to_sym).table_name, columns, Object.const_get(name.to_sym).primary_key rescue nil
|
43
45
|
end
|
44
46
|
end
|
45
|
-
|
47
|
+
|
46
48
|
def create_models(*names)
|
47
49
|
names.each do |name|
|
48
50
|
create_model name
|
49
51
|
end
|
50
52
|
end
|
51
|
-
|
53
|
+
|
52
54
|
def collect_stdout
|
53
55
|
stdout = $stdout
|
54
56
|
$stdout = StringIO.new
|
@@ -58,14 +60,14 @@ class ActiveSupport::TestCase
|
|
58
60
|
ensure
|
59
61
|
$stdout = stdout
|
60
62
|
end
|
61
|
-
|
63
|
+
|
62
64
|
def create_simple_domain
|
63
65
|
create_model "Beer", :bar => :references do
|
64
66
|
belongs_to :bar
|
65
67
|
end
|
66
68
|
create_model "Bar"
|
67
69
|
end
|
68
|
-
|
70
|
+
|
69
71
|
def create_one_to_one_assoc_domain
|
70
72
|
create_model "One" do
|
71
73
|
has_one :other
|
@@ -93,12 +95,12 @@ class ActiveSupport::TestCase
|
|
93
95
|
end
|
94
96
|
create_table "manies_mores", :many_id => :integer, :more_id => :integer
|
95
97
|
end
|
96
|
-
|
98
|
+
|
97
99
|
def create_specialization
|
98
100
|
create_model "Beverage", :type => :string
|
99
101
|
Object.const_set :Beer, Class.new(Beverage)
|
100
102
|
end
|
101
|
-
|
103
|
+
|
102
104
|
def create_generalization
|
103
105
|
create_model "Cannon"
|
104
106
|
create_model "Galleon" do
|
@@ -107,18 +109,19 @@ class ActiveSupport::TestCase
|
|
107
109
|
end
|
108
110
|
|
109
111
|
private
|
110
|
-
|
112
|
+
|
111
113
|
def reset_domain
|
112
114
|
if defined? ActiveRecord
|
113
115
|
ActiveRecord::Base.descendants.each do |model|
|
116
|
+
model.reset_column_information
|
114
117
|
Object.send :remove_const, model.name.to_sym
|
115
118
|
end
|
116
119
|
ActiveRecord::Base.connection.tables.each do |table|
|
117
120
|
ActiveRecord::Base.connection.drop_table table
|
118
121
|
end
|
119
122
|
ActiveRecord::Base.direct_descendants.clear
|
120
|
-
Arel::Relation.send :class_variable_set, :@@connection_tables_primary_keys, {}
|
121
123
|
ActiveSupport::Dependencies::Reference.clear!
|
124
|
+
ActiveRecord::Base.clear_cache!
|
122
125
|
end
|
123
126
|
end
|
124
127
|
end
|