rails-erd 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ doc
@@ -0,0 +1,4 @@
1
+ === 0.1.0
2
+
3
+ * Released on September 20th, 2010.
4
+ * First public release.
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.
@@ -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.
@@ -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
@@ -0,0 +1 @@
1
+ require "rails_erd"
@@ -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