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.
@@ -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