rails-erd 0.4.5 → 1.0.0
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.
- 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
|