rails-erd 0.1.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/.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
|