rails-erd 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/CHANGES.rdoc +4 -0
- data/LICENSE +19 -0
- data/README.rdoc +56 -0
- data/Rakefile +38 -0
- data/VERSION +1 -0
- data/lib/rails-erd.rb +1 -0
- data/lib/rails_erd.rb +38 -0
- data/lib/rails_erd/attribute.rb +74 -0
- data/lib/rails_erd/diagram.rb +88 -0
- data/lib/rails_erd/domain.rb +102 -0
- data/lib/rails_erd/entity.rb +50 -0
- data/lib/rails_erd/railtie.rb +7 -0
- data/lib/rails_erd/relationship.rb +85 -0
- data/lib/rails_erd/relationship/cardinality.rb +35 -0
- data/lib/rails_erd/tasks.rake +38 -0
- data/lib/rails_erd/templates/node.erb +13 -0
- data/test/test_helper.rb +72 -0
- data/test/unit/attribute_test.rb +144 -0
- data/test/unit/cardinality_test.rb +8 -0
- data/test/unit/diagram_test.rb +0 -0
- data/test/unit/domain_test.rb +125 -0
- data/test/unit/entity_test.rb +82 -0
- data/test/unit/relationship_test.rb +308 -0
- metadata +123 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
doc
|
data/CHANGES.rdoc
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2010 Voormedia
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
= Rails ERD - Generate Entity-Relationship Diagrams for Rails applications
|
2
|
+
|
3
|
+
Rails ERD is a Rails plugin that allows you to easily generate diagrams based
|
4
|
+
on your ActiveRecord models. The diagrams give a great overview of how your
|
5
|
+
models are related. Having a diagram that describes your models is perfect
|
6
|
+
documentation for your application.
|
7
|
+
|
8
|
+
Rails ERD was created specifically for Rails 3. It uses ActiveRecord reflection
|
9
|
+
to figure out how your models are associated.
|
10
|
+
|
11
|
+
== Getting started
|
12
|
+
|
13
|
+
In its most simple form, Rails ERD is a plugin for Rails 3 that provides you
|
14
|
+
with a Rake task to create an Entity-Relationship Diagram. It depends on the
|
15
|
+
Graphviz visualisation library. You have to install Graphviz before using
|
16
|
+
Rails ERD. In order to create PDF files (the default), you should install
|
17
|
+
or compile Graphviz with Pango/Cairo.
|
18
|
+
|
19
|
+
For example, to install Graphviz with MacPorts:
|
20
|
+
|
21
|
+
% sudo port install graphviz
|
22
|
+
|
23
|
+
Or with Homebrew:
|
24
|
+
|
25
|
+
% brew install cairo pango graphviz
|
26
|
+
|
27
|
+
Next, install Rails ERD. Open your +Gemfile+, and add the following:
|
28
|
+
|
29
|
+
group :development do
|
30
|
+
gem 'rails-erd'
|
31
|
+
end
|
32
|
+
|
33
|
+
Tell Bundler to install Rails ERD:
|
34
|
+
|
35
|
+
% bundle install
|
36
|
+
|
37
|
+
You now have access to Rails ERD through Rake. Generate a new
|
38
|
+
Entity-Relationship Diagram for your Rails application:
|
39
|
+
|
40
|
+
% rake erd
|
41
|
+
|
42
|
+
All done! You will now have a file named +ERD.pdf+ in your application root.
|
43
|
+
|
44
|
+
== Advanced use
|
45
|
+
|
46
|
+
Rails ERD has several options that you can use to customise its behaviour.
|
47
|
+
All options can be provided on the command line. For example:
|
48
|
+
|
49
|
+
% rake erd exclude_timestamps=false
|
50
|
+
|
51
|
+
For an overview of all available options, see the documentation of RailsERD:
|
52
|
+
http://rails-erd.rubyforge.org/doc/
|
53
|
+
|
54
|
+
== License
|
55
|
+
|
56
|
+
Rails ERD is released under the MIT license.
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "jeweler"
|
3
|
+
require "rake/testtask"
|
4
|
+
|
5
|
+
Jeweler::Tasks.new do |spec|
|
6
|
+
spec.name = "rails-erd"
|
7
|
+
spec.rubyforge_project = "rails-erd"
|
8
|
+
spec.summary = "Entity-relationship diagram for your Rails models."
|
9
|
+
spec.description = "Automatically generate an entity-relationship diagram (ERD) for the models in your Rails application."
|
10
|
+
|
11
|
+
spec.authors = ["Rolf Timmermans"]
|
12
|
+
spec.email = "r.timmermans@voormedia.com"
|
13
|
+
spec.homepage = "http://rails-erd.rubyforge.org/"
|
14
|
+
|
15
|
+
spec.add_runtime_dependency "activesupport", "~> 3.0.0"
|
16
|
+
spec.add_runtime_dependency "ruby-graphviz", "~> 0.9.17"
|
17
|
+
end
|
18
|
+
|
19
|
+
Jeweler::GemcutterTasks.new
|
20
|
+
|
21
|
+
Jeweler::RubyforgeTasks.new do |rubyforge|
|
22
|
+
rubyforge.doc_task = "rdoc"
|
23
|
+
end
|
24
|
+
|
25
|
+
Rake::TestTask.new do |test|
|
26
|
+
test.pattern = "test/unit/**/*_test.rb"
|
27
|
+
end
|
28
|
+
|
29
|
+
begin
|
30
|
+
require "hanna/rdoctask"
|
31
|
+
Rake::RDocTask.new do |rdoc|
|
32
|
+
rdoc.rdoc_files = Dir["[A-Z][A-Z]*"] + Dir["lib/**/*.rb"]
|
33
|
+
rdoc.title = "Rails ERD – Entity-Relationship Diagrams for Rails"
|
34
|
+
rdoc.rdoc_dir = "doc"
|
35
|
+
end
|
36
|
+
rescue => e
|
37
|
+
puts e.message
|
38
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/rails-erd.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "rails_erd"
|
data/lib/rails_erd.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require "active_support/ordered_options"
|
2
|
+
require "rails_erd/railtie" if defined? Rails
|
3
|
+
|
4
|
+
# Rails ERD provides several options that allow you to customise the
|
5
|
+
# generation of the diagram and the domain model itself. Currently, the
|
6
|
+
# following options are supported:
|
7
|
+
#
|
8
|
+
# type:: The file type of the generated diagram. Defaults to +:pdf+, which
|
9
|
+
# is the recommended format. Other formats may render significantly
|
10
|
+
# worse than a PDF file.
|
11
|
+
# orientation:: The direction of the hierarchy of entities. Either +:horizontal+
|
12
|
+
# or +:vertical+. Defaults to +:horizontal+.
|
13
|
+
# suppress_warnings:: When set to +true+, no warnings are printed to the
|
14
|
+
# command line while processing the domain model. Defaults
|
15
|
+
# to +false+.
|
16
|
+
# exclude_timestamps:: Excludes timestamp columns (<tt>created_at/on</tt> and
|
17
|
+
# <tt>updated_at/on</tt>) from attribute lists. Defaults
|
18
|
+
# to +true+.
|
19
|
+
# exclude_primary_keys:: Excludes primary key columns from attribute lists.
|
20
|
+
# Defaults to +true+.
|
21
|
+
# exclude_foreign_keys:: Excludes foreign key columns from attribute lists.
|
22
|
+
# Defaults to +true+.
|
23
|
+
module RailsERD
|
24
|
+
class << self
|
25
|
+
# Access to default options. Any instance of RailsERD::Domain and
|
26
|
+
# RailsERD::Diagram will use these options unless overridden.
|
27
|
+
attr_accessor :options
|
28
|
+
end
|
29
|
+
|
30
|
+
self.options = ActiveSupport::OrderedOptions[
|
31
|
+
:type, :pdf,
|
32
|
+
:orientation, :horizontal,
|
33
|
+
:exclude_timestamps, true,
|
34
|
+
:exclude_primary_keys, true,
|
35
|
+
:exclude_foreign_keys, true,
|
36
|
+
:suppress_warnings, false
|
37
|
+
]
|
38
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# -*- encoding: utf-8
|
2
|
+
module RailsERD
|
3
|
+
class Attribute
|
4
|
+
TIMESTAMP_NAMES = %w{created_at created_on updated_at updated_on} #:nodoc:
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def from_model(domain, model) #:nodoc:
|
8
|
+
model.arel_table.columns.collect { |column| Attribute.new(domain, model, column) }.sort
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :column #:nodoc:
|
13
|
+
|
14
|
+
def initialize(domain, model, column)
|
15
|
+
@domain, @model, @column = domain, model, column
|
16
|
+
end
|
17
|
+
|
18
|
+
def name
|
19
|
+
column.name
|
20
|
+
end
|
21
|
+
|
22
|
+
def type
|
23
|
+
column.type
|
24
|
+
end
|
25
|
+
|
26
|
+
def mandatory?
|
27
|
+
!column.null or @model.validators_on(name).map(&:kind).include?(:presence)
|
28
|
+
end
|
29
|
+
|
30
|
+
def primary_key?
|
31
|
+
@model.arel_table.primary_key == name
|
32
|
+
end
|
33
|
+
|
34
|
+
def foreign_key?
|
35
|
+
@domain.relationships_for(@model).map(&:associations).flatten.map(&:primary_key_name).include?(name)
|
36
|
+
end
|
37
|
+
|
38
|
+
def timestamp?
|
39
|
+
TIMESTAMP_NAMES.include? name
|
40
|
+
end
|
41
|
+
|
42
|
+
def <=>(other) #:nodoc:
|
43
|
+
name <=> other.name
|
44
|
+
end
|
45
|
+
|
46
|
+
def inspect #:nodoc:
|
47
|
+
"#<#{self.class.name}:0x%.14x @column=#{name.inspect} @type=#{type.inspect}>" % (object_id << 1)
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_s #:nodoc:
|
51
|
+
name
|
52
|
+
end
|
53
|
+
|
54
|
+
def type_description
|
55
|
+
case type
|
56
|
+
when :integer then "int"
|
57
|
+
when :float then "float"
|
58
|
+
when :decimal then "dec"
|
59
|
+
when :datetime then "datetime"
|
60
|
+
when :date then "date"
|
61
|
+
when :timestamp then "timest"
|
62
|
+
when :time then "time"
|
63
|
+
when :text then "txt"
|
64
|
+
when :string then "str"
|
65
|
+
when :binary then "blob"
|
66
|
+
when :boolean then "bool"
|
67
|
+
else type.to_s
|
68
|
+
end.tap do |desc|
|
69
|
+
desc << " (#{column.limit})" if column.limit != @model.connection.native_database_types[type][:limit]
|
70
|
+
desc << " ∗" if mandatory? # Add a hair space + low asterisk (Unicode characters).
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require "rails_erd/domain"
|
2
|
+
require "graphviz"
|
3
|
+
|
4
|
+
module RailsERD
|
5
|
+
class Diagram
|
6
|
+
NODE_LABEL_TEMPLATE = File.read(File.expand_path("templates/node.erb", File.dirname(__FILE__))) #:nodoc:
|
7
|
+
NODE_WIDTH = 130 #:nodoc:
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def generate(options = {})
|
11
|
+
new(Domain.generate(options), options).output
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :options #:nodoc:
|
16
|
+
|
17
|
+
def initialize(domain, options = {})
|
18
|
+
@domain, @options = domain, RailsERD.options.merge(options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def graph
|
22
|
+
@graph ||= GraphViz.new(@domain.name, :type => :digraph) do |graph|
|
23
|
+
graph[:rankdir] = horizontal? ? :LR : :TB
|
24
|
+
graph[:ranksep] = 0.5
|
25
|
+
graph[:nodesep] = 0.35
|
26
|
+
graph[:margin] = "0.4,0.4"
|
27
|
+
graph[:concentrate] = true
|
28
|
+
graph[:label] = "#{@domain.name} domain model\\n\\n"
|
29
|
+
graph[:labelloc] = :t
|
30
|
+
graph[:fontsize] = 13
|
31
|
+
graph[:fontname] = "Arial Bold"
|
32
|
+
graph[:remincross] = true
|
33
|
+
graph[:outputorder] = :edgesfirst
|
34
|
+
|
35
|
+
graph.node[:shape] = "Mrecord"
|
36
|
+
graph.node[:fontsize] = 10
|
37
|
+
graph.node[:fontname] = "Arial"
|
38
|
+
graph.node[:margin] = "0.07,0.05"
|
39
|
+
|
40
|
+
graph.edge[:fontname] = "Arial"
|
41
|
+
graph.edge[:fontsize] = 8
|
42
|
+
graph.edge[:dir] = :both
|
43
|
+
graph.edge[:arrowsize] = 0.8
|
44
|
+
|
45
|
+
nodes = {}
|
46
|
+
|
47
|
+
@domain.entities.select(&:connected?).each do |entity|
|
48
|
+
attributes = entity.attributes.reject { |attribute|
|
49
|
+
options.exclude_primary_keys && attribute.primary_key? or
|
50
|
+
options.exclude_foreign_keys && attribute.foreign_key? or
|
51
|
+
options.exclude_timestamps && attribute.timestamp?
|
52
|
+
}
|
53
|
+
|
54
|
+
nodes[entity] = graph.add_node entity.name, :html => ERB.new(NODE_LABEL_TEMPLATE, nil, "<>").result(binding)
|
55
|
+
end
|
56
|
+
|
57
|
+
@domain.relationships.each do |relationship|
|
58
|
+
options = {}
|
59
|
+
options[:arrowhead] = relationship.cardinality.one_to_one? ? :dot : :normal
|
60
|
+
options[:arrowtail] = relationship.cardinality.many_to_many? ? :normal : :dot
|
61
|
+
options[:weight] = relationship.strength
|
62
|
+
options.merge! :style => :dashed, :constraint => false if relationship.indirect?
|
63
|
+
|
64
|
+
graph.add_edge nodes[relationship.source], nodes[relationship.destination], options
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def output
|
70
|
+
graph.output(options.type.to_sym => file_name)
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def file_name
|
75
|
+
"ERD.#{options.type}"
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def horizontal?
|
81
|
+
options.orientation == :horizontal
|
82
|
+
end
|
83
|
+
|
84
|
+
def vertical?
|
85
|
+
!horizontal?
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require "set"
|
2
|
+
require "rails_erd"
|
3
|
+
require "rails_erd/entity"
|
4
|
+
require "rails_erd/relationship"
|
5
|
+
require "rails_erd/relationship/cardinality"
|
6
|
+
require "rails_erd/attribute"
|
7
|
+
|
8
|
+
module RailsERD
|
9
|
+
# The domain describes your Rails domain model. This class is the starting
|
10
|
+
# point to get information about your models.
|
11
|
+
class Domain
|
12
|
+
class << self
|
13
|
+
# Generates a domain model object based on all loaded subclasses of
|
14
|
+
# <tt>ActiveRecord::Base</tt>. Make sure your models are loaded before calling
|
15
|
+
# this method.
|
16
|
+
#
|
17
|
+
# The +options+ hash allows you to override the default options. For a
|
18
|
+
# list of available options, see RailsERD.
|
19
|
+
def generate(options = {})
|
20
|
+
new ActiveRecord::Base.descendants, options
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :options #:nodoc:
|
25
|
+
|
26
|
+
# Create a new domain model object based on the given array of models.
|
27
|
+
# The given models are assumed to be subclasses of <tt>ActiveRecord::Base</tt>.
|
28
|
+
def initialize(models = [], options = {})
|
29
|
+
@models, @options = models, RailsERD.options.merge(options)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the domain model name, which is the name of your Rails
|
33
|
+
# application or +nil+ outside of Rails.
|
34
|
+
def name
|
35
|
+
defined? Rails and Rails.application and Rails.application.class.parent.name
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns all entities of your domain model.
|
39
|
+
def entities
|
40
|
+
@entities ||= entity_mapping.values.sort
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns all relationships in your domain model.
|
44
|
+
def relationships
|
45
|
+
@relationships ||= Relationship.from_associations(self, associations)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns a specific entity object for the given +ActiveRecord+ model.
|
49
|
+
def entity_for(model) #:nodoc:
|
50
|
+
entity_mapping[model] or raise "model #{model} exists, but is not included in the domain"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns an array of relationships for the given +ActiveRecord+ model.
|
54
|
+
def relationships_for(model) #:nodoc:
|
55
|
+
relationships_mapping[model] or []
|
56
|
+
end
|
57
|
+
|
58
|
+
def inspect #:nodoc:
|
59
|
+
"#<#{self.class} {#{relationships.map { |rel| "#{rel.from} => #{rel.to}" } * ", "}}>"
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def entity_mapping
|
65
|
+
@entity_mapping ||= Hash[@models.collect { |model| [model, Entity.new(self, model)] }]
|
66
|
+
end
|
67
|
+
|
68
|
+
def relationships_mapping
|
69
|
+
@relationships_mapping ||= {}.tap do |mapping|
|
70
|
+
relationships.each do |relationship|
|
71
|
+
(mapping[relationship.source.model] ||= []) << relationship
|
72
|
+
(mapping[relationship.destination.model] ||= []) << relationship
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def associations
|
78
|
+
@associations ||= @models.collect(&:reflect_on_all_associations).flatten.select { |assoc| check_association_validity(assoc) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def check_association_validity(association)
|
82
|
+
# Raises an ActiveRecord::ActiveRecordError if the association is broken.
|
83
|
+
association.check_validity!
|
84
|
+
|
85
|
+
# Raises NameError if the associated class cannot be found.
|
86
|
+
model = association.klass
|
87
|
+
|
88
|
+
# Raises error if model is not in the domain.
|
89
|
+
entity_for model
|
90
|
+
rescue => e
|
91
|
+
warn "Invalid association #{association_description(association)} (#{e.message})"
|
92
|
+
end
|
93
|
+
|
94
|
+
def warn(message)
|
95
|
+
puts "Warning: #{message}" unless options.suppress_warnings
|
96
|
+
end
|
97
|
+
|
98
|
+
def association_description(association)
|
99
|
+
"#{association.name.inspect} on #{association.active_record}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module RailsERD
|
2
|
+
# Entities represent your +ActiveRecord+ models. Entities may be connected
|
3
|
+
# to other entities.
|
4
|
+
class Entity
|
5
|
+
# The domain in which this entity resides.
|
6
|
+
attr_reader :domain
|
7
|
+
|
8
|
+
# The +ActiveRecord+ model that this entity corresponds to.
|
9
|
+
attr_reader :model
|
10
|
+
|
11
|
+
def initialize(domain, model) #:nodoc:
|
12
|
+
@domain, @model = domain, model
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns an array of attributes for this entity.
|
16
|
+
def attributes
|
17
|
+
@attributes ||= Attribute.from_model @domain, @model
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns an array of all relationships that this entity has with other
|
21
|
+
# entities in the domain model.
|
22
|
+
def relationships
|
23
|
+
@domain.relationships_for(@model)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns +true+ if this entity has any relationships with other models,
|
27
|
+
# +false+ otherwise.
|
28
|
+
def connected?
|
29
|
+
relationships.any?
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the name of this entity, which is the class name of the
|
33
|
+
# corresponding model.
|
34
|
+
def name
|
35
|
+
model.name
|
36
|
+
end
|
37
|
+
|
38
|
+
def inspect #:nodoc:
|
39
|
+
"#<#{self.class}:0x%.14x @model=#{name}>" % (object_id << 1)
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s #:nodoc:
|
43
|
+
name
|
44
|
+
end
|
45
|
+
|
46
|
+
def <=>(other) #:nodoc:
|
47
|
+
self.name <=> other.name
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|