mermaid_rails_erd 1.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 54915650b33e2be8a680722f466382e0f2671a4c9a1d023e18a26fea744fb03b
4
+ data.tar.gz: 78f1a7ec38a563280a5b714bd14c6d18b6819c24a89b0ffd4015a744a87e356d
5
+ SHA512:
6
+ metadata.gz: a9e598ba9b70ae93b98dfbdb60b1b6f5d4ef5e706b13c5f6cb7ac4e0a14c6adfc622952916b10a514d830c42f0f00dc57e05e817f3e5d3143607c26b3d95f841
7
+ data.tar.gz: 6c5e1e893393a4a52c9c25c8ba9295ef97ff268690298d649ba2231955ea6a81509604064896e5891705a488c6e4f1db394a221477815a5e88726bdb6f0e4b28
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Yang Liu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # Mermaid Rails ERD
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/mermaid_rails_erd.svg)](https://badge.fury.io/rb/mermaid_rails_erd)
4
+ [![CI](https://github.com/delexw/mermaid_rails_erd/workflows/CI/badge.svg)](https://github.com/delexw/mermaid_rails_erd/actions)
5
+
6
+ A Ruby gem that generates [Mermaid.js](https://mermaid.js.org/) Entity Relationship Diagrams (ERD) from ActiveRecord models in Rails applications.
7
+
8
+ ## Features
9
+
10
+ - 🔍 **ActiveRecord Introspection**: Automatically discovers your Rails models and their relationships
11
+ - 📊 **Mermaid ERD Generation**: Outputs clean, readable Mermaid.js ERD syntax
12
+ - 🚀 **Simple Integration**: Easy-to-use Rake task for generating diagrams
13
+ - 🔗 **Relationship Mapping**: Supports `belongs_to`, `has_one`, `has_many`, and `has_and_belongs_to_many` associations
14
+ - 💫 **Polymorphic Support**: Accurately discovers and maps polymorphic relationships
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem 'mermaid_rails_erd', group: :development
22
+ ```
23
+
24
+ And then execute:
25
+
26
+ ```bash
27
+ $ bundle install
28
+ ```
29
+
30
+ Or install it yourself as:
31
+
32
+ ```bash
33
+ $ gem install mermaid_rails_erd
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ### Generate ERD with Rake Task
39
+
40
+ The simplest way to generate your ERD is using the provided Rake task:
41
+
42
+ ```bash
43
+ $ bundle exec rails mermaid_rails_erd:generate
44
+ ```
45
+
46
+ This will:
47
+ 1. Analyze all your ActiveRecord models
48
+ 2. Generate a Mermaid ERD file at `tmp/erd.mmd`
49
+ 3. Provide instructions on how to view the diagram
50
+
51
+
52
+ ### Advanced Usage: Model Data Interface
53
+
54
+ You can access the collected model and relationship data directly without generating a diagram:
55
+
56
+ #### Simple Data Collection
57
+
58
+ ```ruby
59
+ # Get all collected data in a structured format
60
+ data = MermaidRailsErd.build.parsed_data
61
+
62
+ # Access collected data
63
+ models_data = data.models_data # Hash of models having table keyed by model name
64
+ models = data.models # Array of all loaded models
65
+ models_no_tables = data.models_no_tables # Array of models missing tables
66
+ relationships = data.relationships # Array of relationship objects
67
+ invalid_associations = data.invalid_associations # Array of associations missing associated table
68
+ polymorphic_associations = data.polymorphic_associations # Array of polymorphic associations
69
+ regular_associations = data.regular_associations # Array of regular (non-polymorphic) associations
70
+
71
+ ```
72
+
73
+ ## Viewing the Generated ERD
74
+
75
+ Once you have generated the `.mmd` file, you can view it using:
76
+
77
+ - **Mermaid ERD Visulizer**: https://github.com/delexw/mermaid-erd-visualizer
78
+
79
+
80
+ ## Example Output
81
+
82
+ Given Rails models like:
83
+
84
+ ```ruby
85
+ class User < ActiveRecord::Base
86
+ has_many :posts
87
+ has_one :profile
88
+ end
89
+
90
+ class Post < ActiveRecord::Base
91
+ belongs_to :user
92
+ has_many :comments
93
+ end
94
+
95
+ class Comment < ActiveRecord::Base
96
+ belongs_to :post
97
+ end
98
+
99
+ class Profile < ActiveRecord::Base
100
+ belongs_to :user
101
+ end
102
+ ```
103
+
104
+ The gem will generate:
105
+
106
+ ```mermaid
107
+ erDiagram
108
+ users {
109
+ bigint id PK
110
+ varchar email
111
+ varchar name
112
+ timestamp created_at
113
+ timestamp updated_at
114
+ }
115
+
116
+ posts {
117
+ bigint id PK
118
+ bigint user_id
119
+ varchar title
120
+ text content
121
+ timestamp created_at
122
+ timestamp updated_at
123
+ }
124
+
125
+ comments {
126
+ bigint id PK
127
+ bigint post_id
128
+ text content
129
+ timestamp created_at
130
+ timestamp updated_at
131
+ }
132
+
133
+ profiles {
134
+ bigint id PK
135
+ bigint user_id
136
+ text bio
137
+ timestamp created_at
138
+ timestamp updated_at
139
+ }
140
+
141
+ users ||--o{ posts : "user_id"
142
+ posts ||--o{ comments : "post_id"
143
+ users ||--|| profiles : "user_id"
144
+ ```
145
+
146
+ ## Polymorphic Associations
147
+
148
+ The gem automatically detects and properly maps polymorphic associations in your models. For example:
149
+
150
+ ```ruby
151
+ class Comment < ActiveRecord::Base
152
+ belongs_to :commentable, polymorphic: true
153
+ end
154
+
155
+ class Post < ActiveRecord::Base
156
+ has_many :comments, as: :commentable
157
+ end
158
+
159
+ class Photo < ActiveRecord::Base
160
+ has_many :comments, as: :commentable
161
+ end
162
+ ```
163
+
164
+ The ERD will correctly show relationships between `comments` and both `posts` and `photos` tables.
165
+
166
+ ## Supported ActiveRecord Associations
167
+
168
+ - **belongs_to**: Generates one-to-many relationships
169
+ - **has_one**: Generates one-to-one relationships
170
+ - **has_many**: Covered by the corresponding belongs_to
171
+ - **has_and_belongs_to_many**: Generates many-to-many relationships
172
+ - **polymorphic**: Discovers and maps all polymorphic interfaces
173
+
174
+ ## Development
175
+
176
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
177
+
178
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
179
+
180
+ ## Contributing
181
+
182
+ Bug reports and pull requests are welcome on GitHub at https://github.com/delexw/mermaid_rails_erd.
183
+
184
+ ## License
185
+
186
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
187
+
188
+ ## Changelog
189
+
190
+ See [CHANGELOG.md](CHANGELOG.md) for details about changes in each version.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/bin/console ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # This file allows you to interactively experiment with your gem using `bin/console`
5
+ require "bundler/setup"
6
+ require "rails_mermaid_erd"
7
+ require "irb"
8
+
9
+ puts "Loaded rails_mermaid_erd. You can now use the gem in this IRB session."
10
+ IRB.start
data/bin/release ADDED
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+ require "fileutils"
6
+
7
+ class ReleaseManager
8
+ VERSION_FILE = "lib/rails_mermaid_erd/version.rb"
9
+ CHANGELOG_FILE = "CHANGELOG.md"
10
+
11
+ def initialize
12
+ @options = {}
13
+ parse_options
14
+ end
15
+
16
+ def run
17
+ case @options[:action]
18
+ when "bump"
19
+ bump_version(@options[:version_type])
20
+ when "tag"
21
+ create_tag
22
+ when "release"
23
+ full_release(@options[:version_type])
24
+ else
25
+ puts "Unknown action: #{@options[:action]}"
26
+ exit 1
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def parse_options
33
+ OptionParser.new do |opts|
34
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
35
+
36
+ opts.on("-a", "--action ACTION", "Action to perform (bump, tag, release)") do |action|
37
+ @options[:action] = action
38
+ end
39
+
40
+ opts.on("-t", "--type TYPE", "Version type (major, minor, patch)") do |type|
41
+ @options[:version_type] = type
42
+ end
43
+
44
+ opts.on("-h", "--help", "Show this help") do
45
+ puts opts
46
+ exit
47
+ end
48
+ end.parse!
49
+
50
+ return unless @options[:action].nil?
51
+
52
+ puts "Action required. Use -h for help."
53
+ exit 1
54
+ end
55
+
56
+ def current_version
57
+ version_content = File.read(VERSION_FILE)
58
+ version_content.match(/VERSION = ["']([^"']+)["']/)[1]
59
+ end
60
+
61
+ def bump_version(type)
62
+ current = current_version
63
+ parts = current.split(".").map(&:to_i)
64
+
65
+ case type
66
+ when "major"
67
+ parts[0] += 1
68
+ parts[1] = 0
69
+ parts[2] = 0
70
+ when "minor"
71
+ parts[1] += 1
72
+ parts[2] = 0
73
+ when "patch"
74
+ parts[2] += 1
75
+ else
76
+ puts "Invalid version type: #{type}. Use major, minor, or patch."
77
+ exit 1
78
+ end
79
+
80
+ new_version = parts.join(".")
81
+
82
+ # Update version file
83
+ version_content = File.read(VERSION_FILE)
84
+ new_content = version_content.gsub(/VERSION = ["'][^"']+["']/, "VERSION = \"#{new_version}\"")
85
+ File.write(VERSION_FILE, new_content)
86
+
87
+ puts "Version bumped from #{current} to #{new_version}"
88
+ new_version
89
+ end
90
+
91
+ def create_tag
92
+ version = current_version
93
+
94
+ # Create git tag
95
+ system("git add -A")
96
+ system("git commit -m 'Release v#{version}'")
97
+ system("git tag -a v#{version} -m 'Release v#{version}'")
98
+
99
+ puts "Created tag v#{version}"
100
+ puts "Push with: git push origin main --tags"
101
+ end
102
+
103
+ def full_release(type)
104
+ puts "Starting full release process..."
105
+
106
+ # Check if working directory is clean
107
+ unless system("git diff --quiet && git diff --cached --quiet")
108
+ puts "Working directory is not clean. Please commit or stash changes first."
109
+ exit 1
110
+ end
111
+
112
+ # Run tests
113
+ puts "Running tests..."
114
+ unless system("bundle exec rake")
115
+ puts "Tests failed. Aborting release."
116
+ exit 1
117
+ end
118
+
119
+ # Bump version
120
+ new_version = bump_version(type)
121
+
122
+ # Update changelog
123
+ update_changelog(new_version)
124
+
125
+ # Create tag
126
+ create_tag
127
+
128
+ puts "\nRelease v#{new_version} prepared!"
129
+ puts "To complete the release:"
130
+ puts "1. Review the changes: git log --oneline -10"
131
+ puts "2. Push to GitHub: git push origin main --tags"
132
+ puts "3. Create a GitHub release at: https://github.com/delexw/rails_mermaid_erd/releases/new"
133
+ end
134
+
135
+ def update_changelog(version)
136
+ return unless File.exist?(CHANGELOG_FILE)
137
+
138
+ date = Time.now.strftime("%Y-%m-%d")
139
+ changelog_content = File.read(CHANGELOG_FILE)
140
+
141
+ # Find the unreleased section and replace it
142
+ new_content = changelog_content.gsub(
143
+ "## [Unreleased]",
144
+ "## [Unreleased]\n\n## [#{version}] - #{date}",
145
+ )
146
+
147
+ File.write(CHANGELOG_FILE, new_content)
148
+ puts "Updated CHANGELOG.md with version #{version}"
149
+ end
150
+ end
151
+
152
+ ReleaseManager.new.run if __FILE__ == $PROGRAM_NAME
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MermaidRailsErd
4
+ class AssociationResolver
5
+ def resolve(assoc)
6
+ # Try direct table_name access if available
7
+ if assoc.respond_to?(:table_name)
8
+ begin
9
+ table_name = assoc.table_name
10
+ rescue StandardError
11
+ table_name = nil
12
+ end
13
+ end
14
+
15
+ # Determine table name from options or plural_name if not already set
16
+ table_name ||= if assoc.options[:table_name]
17
+ assoc.options[:table_name].to_s
18
+ else
19
+ assoc.plural_name.to_s
20
+ end
21
+
22
+ # Check if table exists
23
+ return nil unless ActiveRecord::Base.connection.table_exists?(table_name)
24
+
25
+ # Return a hash with necessary information
26
+ {
27
+ table_name: table_name,
28
+ primary_key: ActiveRecord::Base.connection.primary_key(table_name),
29
+ }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MermaidRailsErd
4
+ class ColumnInfo
5
+ attr_reader :name, :annotations, :raw_sql_type, :activerecord_type, :isNullable
6
+
7
+ def initialize(name, annotations = [], raw_sql_type = nil, activerecord_type = nil, isNullable = nil)
8
+ @name = name
9
+ @annotations = annotations
10
+ @raw_sql_type = raw_sql_type
11
+ @activerecord_type = activerecord_type
12
+ @isNullable = isNullable
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "relationship"
4
+ require_relative "mermaid_emitter"
5
+ require_relative "column_info"
6
+ require_relative "model_loader"
7
+ require_relative "association_resolver"
8
+ require_relative "polymorphic_targets_resolver"
9
+ require_relative "relationship_symbol_mapper"
10
+ require_relative "relationship_registry"
11
+ require_relative "model_data_collector"
12
+ require_relative "parsed_data"
13
+
14
+ module MermaidRailsErd
15
+ class Generator
16
+ def initialize(output: nil)
17
+ @output = output
18
+ @printed_tables = Set.new
19
+ @printed_relationships = Set.new
20
+ @relationships = []
21
+
22
+ @model_loader = ModelLoader.new
23
+ @association_resolver = AssociationResolver.new
24
+ @symbol_mapper = RelationshipSymbolMapper.new
25
+ @model_data_collector = ModelDataCollector.new(@model_loader)
26
+ @polymorphic_resolver = PolymorphicTargetsResolver.new(@model_data_collector)
27
+ @relationship_registry = RelationshipRegistry.new(
28
+ symbol_mapper: @symbol_mapper,
29
+ association_resolver: @association_resolver,
30
+ polymorphic_resolver: @polymorphic_resolver,
31
+ printed_tables: @printed_tables,
32
+ model_data_collector: @model_data_collector,
33
+ )
34
+ end
35
+
36
+ # Build and collect data from models
37
+ # @return [self]
38
+ def build
39
+ @model_data_collector.collect
40
+
41
+ # Build all relationships with polymorphic handling first
42
+ begin
43
+ @relationships = @relationship_registry.build_all_relationships
44
+ rescue StandardError => e
45
+ puts "ERROR building relationships: #{e.class} - #{e.message}"
46
+ puts e.backtrace.join("\n")
47
+ @relationships = []
48
+ end
49
+
50
+ # Update table definitions with FK annotations
51
+ @tables = @model_data_collector.update_foreign_keys(@relationships)
52
+
53
+ self
54
+ end
55
+
56
+ # Get parsed data as a structured object
57
+ # @return [ParsedData] Struct containing relationships, tables, and delegated model data
58
+ def parsed_data
59
+ ParsedData.new(@relationships, @tables, @model_data_collector)
60
+ end
61
+
62
+ # Generate and emit the ERD diagram
63
+ # @param output [IO] Output stream to write the ERD to (defaults to the one provided in initialize)
64
+ # @return [void]
65
+ def emit(output: nil)
66
+ output ||= @output
67
+ output ||= $stdout
68
+ MermaidEmitter.new(output, @tables, @relationships).emit
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MermaidRailsErd
4
+ class MermaidEmitter
5
+ def initialize(output, tables, relationships)
6
+ @output = output
7
+ @tables = tables
8
+ @relationships = relationships
9
+ end
10
+
11
+ def emit
12
+ @output.puts "erDiagram"
13
+
14
+ @tables.each do |table_name, columns|
15
+ @output.puts " #{table_name} {"
16
+ columns.each do |col|
17
+ annotations = col.annotations.empty? ? "" : " #{col.annotations.join(' ')}"
18
+ @output.puts " #{col.activerecord_type} #{col.name}#{annotations}"
19
+ end
20
+ @output.puts " }"
21
+ end
22
+
23
+ @output.puts
24
+
25
+ emitted = Set.new
26
+ @relationships.each do |rel|
27
+ next if emitted.include?(rel.key)
28
+
29
+ emitted << rel.key
30
+ @output.puts " #{rel.from_table} #{rel.relationship_type} #{rel.to_table} : \"#{rel.label}\""
31
+ end
32
+ end
33
+ end
34
+ end