arerd 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9a3364c03a39be5bf9ac0727652516999efb340d3208fd87621556021c28de20
4
+ data.tar.gz: c84c46b6e3e2fec66857e74c087e5ba6f03b2fe4cc42c3ed99d9997dc5f69038
5
+ SHA512:
6
+ metadata.gz: 8a634f01c34f49fe14acfcfc9e9d37407723852857b05ab16ecef35ac5f5dfb420a6eeaf5695e156d0ca44e998a8f8df7032d8e3f987ab7b51b77b66c4035dc4
7
+ data.tar.gz: 46d6acb3f3be56edb6630df0c97ff1a318d73b6aba02b2e009272ae0761bc616896040f6dd12ebc7beb8918845632274f7df9f70044f6ba8f4b5822f8305b1ee
@@ -0,0 +1,22 @@
1
+ FROM mcr.microsoft.com/devcontainers/base:1-bookworm
2
+
3
+ RUN apt-get update && \
4
+ export DEBIAN_FRONTEND=noninteractive && \
5
+ apt-get -y install --no-install-recommends \
6
+ fish \
7
+ build-essential autoconf libssl-dev libyaml-dev zlib1g-dev libffi-dev libgmp-dev rustc && \
8
+ rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
9
+
10
+ # Install asdf
11
+ RUN curl -L 'https://github.com/asdf-vm/asdf/releases/download/v0.18.0/asdf-v0.18.0-linux-amd64.tar.gz' | tar -xz -C /usr/local/bin && \
12
+ mkdir -p /home/vscode/.config/fish/conf.d && \
13
+ curl -L 'https://raw.githubusercontent.com/asdf-vm/asdf/refs/tags/v0.18.0/asdf.fish' > /home/vscode/.config/fish/conf.d/asdf.fish && \
14
+ chown -R vscode:vscode /home/vscode/.config
15
+
16
+ # Install asdf plugins and specified runtimes
17
+ USER vscode
18
+ WORKDIR /home/vscode
19
+ COPY --chown=vscode:vscode .tool-versions /home/vscode/.tool-versions
20
+ RUN asdf plugin add ruby && \
21
+ asdf install && \
22
+ asdf reshim
@@ -0,0 +1,18 @@
1
+ name: "arerd"
2
+
3
+ services:
4
+ arerd:
5
+ build:
6
+ context: ..
7
+ dockerfile: .devcontainer/Dockerfile
8
+
9
+ volumes:
10
+ - ../..:/workspaces:cached
11
+ - ./fish/conf.d/abbrs.fish:/home/vscode/.config/fish/conf.d/abbrs.fish
12
+ - ./fish/functions/git.fish:/home/vscode/.config/fish/conf.d/git.fish
13
+
14
+ # Overrides default command so things don't shut down after the process ends.
15
+ command: sleep infinity
16
+
17
+ # Uncomment the next line to use a non-root user for all processes.
18
+ user: vscode
@@ -0,0 +1,37 @@
1
+ // For format details, see https://containers.dev/implementors/json_reference/.
2
+ // For config options, see the README at: https://github.com/devcontainers/templates/tree/main/src/ruby
3
+ {
4
+ "name": "arerd",
5
+ "dockerComposeFile": "compose.yaml",
6
+ "service": "arerd",
7
+ "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
8
+
9
+ // Features to add to the dev container. More info: https://containers.dev/features.
10
+ "features": {
11
+ "ghcr.io/devcontainers/features/github-cli:1": {}
12
+ },
13
+
14
+ "containerEnv": {},
15
+
16
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
17
+ "forwardPorts": [],
18
+
19
+ // Configure tool-specific properties.
20
+ "customizations": {
21
+ "vscode": {
22
+ "settings": {
23
+ "terminal.integrated.defaultProfile.linux": "fish"
24
+ },
25
+ "extensions": [
26
+ "EditorConfig.EditorConfig",
27
+ "Shopify.ruby-lsp",
28
+ ]
29
+ }
30
+ },
31
+
32
+ // Uncomment to connect as root instead. More info: https://containers.dev/implementors/json_reference/#remoteUser.
33
+ // "remoteUser": "root",
34
+
35
+ // Use 'postCreateCommand' to run commands after the container is created.
36
+ "postCreateCommand": "./.devcontainer/post_create.fish"
37
+ }
@@ -0,0 +1,3 @@
1
+ if type -q git
2
+ abbr -a g git
3
+ end
@@ -0,0 +1,12 @@
1
+ if type -q git
2
+ function git
3
+ if test (count $argv) -eq 0
4
+ git status
5
+ else if test $argv[1] = "up"
6
+ git rev-parse --is-inside-work-tree > /dev/null 2>&1 && \
7
+ cd (pwd)/(git rev-parse --show-cdup)
8
+ else
9
+ command git $argv
10
+ end
11
+ end
12
+ end
@@ -0,0 +1 @@
1
+ #!/bin/fish
data/.editorconfig ADDED
@@ -0,0 +1,12 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.{md,markdown}]
12
+ trim_trailing_whitespace = false
data/.standard.yml ADDED
@@ -0,0 +1,2 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 3.1.6
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in arerd.gemspec
6
+ gemspec
7
+
8
+ group :development, :test do
9
+ gem "irb"
10
+ gem "rake"
11
+ gem "minitest"
12
+ gem "standard"
13
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,248 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ arerd (0.1.0)
5
+ rails (>= 6.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ actioncable (7.2.2.1)
11
+ actionpack (= 7.2.2.1)
12
+ activesupport (= 7.2.2.1)
13
+ nio4r (~> 2.0)
14
+ websocket-driver (>= 0.6.1)
15
+ zeitwerk (~> 2.6)
16
+ actionmailbox (7.2.2.1)
17
+ actionpack (= 7.2.2.1)
18
+ activejob (= 7.2.2.1)
19
+ activerecord (= 7.2.2.1)
20
+ activestorage (= 7.2.2.1)
21
+ activesupport (= 7.2.2.1)
22
+ mail (>= 2.8.0)
23
+ actionmailer (7.2.2.1)
24
+ actionpack (= 7.2.2.1)
25
+ actionview (= 7.2.2.1)
26
+ activejob (= 7.2.2.1)
27
+ activesupport (= 7.2.2.1)
28
+ mail (>= 2.8.0)
29
+ rails-dom-testing (~> 2.2)
30
+ actionpack (7.2.2.1)
31
+ actionview (= 7.2.2.1)
32
+ activesupport (= 7.2.2.1)
33
+ nokogiri (>= 1.8.5)
34
+ racc
35
+ rack (>= 2.2.4, < 3.2)
36
+ rack-session (>= 1.0.1)
37
+ rack-test (>= 0.6.3)
38
+ rails-dom-testing (~> 2.2)
39
+ rails-html-sanitizer (~> 1.6)
40
+ useragent (~> 0.16)
41
+ actiontext (7.2.2.1)
42
+ actionpack (= 7.2.2.1)
43
+ activerecord (= 7.2.2.1)
44
+ activestorage (= 7.2.2.1)
45
+ activesupport (= 7.2.2.1)
46
+ globalid (>= 0.6.0)
47
+ nokogiri (>= 1.8.5)
48
+ actionview (7.2.2.1)
49
+ activesupport (= 7.2.2.1)
50
+ builder (~> 3.1)
51
+ erubi (~> 1.11)
52
+ rails-dom-testing (~> 2.2)
53
+ rails-html-sanitizer (~> 1.6)
54
+ activejob (7.2.2.1)
55
+ activesupport (= 7.2.2.1)
56
+ globalid (>= 0.3.6)
57
+ activemodel (7.2.2.1)
58
+ activesupport (= 7.2.2.1)
59
+ activerecord (7.2.2.1)
60
+ activemodel (= 7.2.2.1)
61
+ activesupport (= 7.2.2.1)
62
+ timeout (>= 0.4.0)
63
+ activestorage (7.2.2.1)
64
+ actionpack (= 7.2.2.1)
65
+ activejob (= 7.2.2.1)
66
+ activerecord (= 7.2.2.1)
67
+ activesupport (= 7.2.2.1)
68
+ marcel (~> 1.0)
69
+ activesupport (7.2.2.1)
70
+ base64
71
+ benchmark (>= 0.3)
72
+ bigdecimal
73
+ concurrent-ruby (~> 1.0, >= 1.3.1)
74
+ connection_pool (>= 2.2.5)
75
+ drb
76
+ i18n (>= 1.6, < 2)
77
+ logger (>= 1.4.2)
78
+ minitest (>= 5.1)
79
+ securerandom (>= 0.3)
80
+ tzinfo (~> 2.0, >= 2.0.5)
81
+ ast (2.4.3)
82
+ base64 (0.3.0)
83
+ benchmark (0.4.1)
84
+ bigdecimal (3.2.2)
85
+ builder (3.3.0)
86
+ cgi (0.5.0)
87
+ concurrent-ruby (1.3.5)
88
+ connection_pool (2.5.3)
89
+ crass (1.0.6)
90
+ date (3.4.1)
91
+ drb (2.2.3)
92
+ erb (4.0.4)
93
+ cgi (>= 0.3.3)
94
+ erubi (1.13.1)
95
+ globalid (1.2.1)
96
+ activesupport (>= 6.1)
97
+ i18n (1.14.7)
98
+ concurrent-ruby (~> 1.0)
99
+ io-console (0.8.0)
100
+ irb (1.15.2)
101
+ pp (>= 0.6.0)
102
+ rdoc (>= 4.0.0)
103
+ reline (>= 0.4.2)
104
+ json (2.12.2)
105
+ language_server-protocol (3.17.0.5)
106
+ lint_roller (1.1.0)
107
+ logger (1.7.0)
108
+ loofah (2.24.1)
109
+ crass (~> 1.0.2)
110
+ nokogiri (>= 1.12.0)
111
+ mail (2.8.1)
112
+ mini_mime (>= 0.1.1)
113
+ net-imap
114
+ net-pop
115
+ net-smtp
116
+ marcel (1.0.4)
117
+ mini_mime (1.1.5)
118
+ minitest (5.25.5)
119
+ net-imap (0.5.9)
120
+ date
121
+ net-protocol
122
+ net-pop (0.1.2)
123
+ net-protocol
124
+ net-protocol (0.2.2)
125
+ timeout
126
+ net-smtp (0.5.1)
127
+ net-protocol
128
+ nio4r (2.7.4)
129
+ nokogiri (1.18.8-x86_64-linux-gnu)
130
+ racc (~> 1.4)
131
+ parallel (1.27.0)
132
+ parser (3.3.8.0)
133
+ ast (~> 2.4.1)
134
+ racc
135
+ pp (0.6.2)
136
+ prettyprint
137
+ prettyprint (0.2.0)
138
+ prism (1.4.0)
139
+ psych (5.2.6)
140
+ date
141
+ stringio
142
+ racc (1.8.1)
143
+ rack (3.1.16)
144
+ rack-session (2.1.1)
145
+ base64 (>= 0.1.0)
146
+ rack (>= 3.0.0)
147
+ rack-test (2.2.0)
148
+ rack (>= 1.3)
149
+ rackup (2.2.1)
150
+ rack (>= 3)
151
+ rails (7.2.2.1)
152
+ actioncable (= 7.2.2.1)
153
+ actionmailbox (= 7.2.2.1)
154
+ actionmailer (= 7.2.2.1)
155
+ actionpack (= 7.2.2.1)
156
+ actiontext (= 7.2.2.1)
157
+ actionview (= 7.2.2.1)
158
+ activejob (= 7.2.2.1)
159
+ activemodel (= 7.2.2.1)
160
+ activerecord (= 7.2.2.1)
161
+ activestorage (= 7.2.2.1)
162
+ activesupport (= 7.2.2.1)
163
+ bundler (>= 1.15.0)
164
+ railties (= 7.2.2.1)
165
+ rails-dom-testing (2.3.0)
166
+ activesupport (>= 5.0.0)
167
+ minitest
168
+ nokogiri (>= 1.6)
169
+ rails-html-sanitizer (1.6.2)
170
+ loofah (~> 2.21)
171
+ nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
172
+ railties (7.2.2.1)
173
+ actionpack (= 7.2.2.1)
174
+ activesupport (= 7.2.2.1)
175
+ irb (~> 1.13)
176
+ rackup (>= 1.0.0)
177
+ rake (>= 12.2)
178
+ thor (~> 1.0, >= 1.2.2)
179
+ zeitwerk (~> 2.6)
180
+ rainbow (3.1.1)
181
+ rake (13.3.0)
182
+ rdoc (6.14.2)
183
+ erb
184
+ psych (>= 4.0.0)
185
+ regexp_parser (2.10.0)
186
+ reline (0.6.1)
187
+ io-console (~> 0.5)
188
+ rubocop (1.75.8)
189
+ json (~> 2.3)
190
+ language_server-protocol (~> 3.17.0.2)
191
+ lint_roller (~> 1.1.0)
192
+ parallel (~> 1.10)
193
+ parser (>= 3.3.0.2)
194
+ rainbow (>= 2.2.2, < 4.0)
195
+ regexp_parser (>= 2.9.3, < 3.0)
196
+ rubocop-ast (>= 1.44.0, < 2.0)
197
+ ruby-progressbar (~> 1.7)
198
+ unicode-display_width (>= 2.4.0, < 4.0)
199
+ rubocop-ast (1.45.1)
200
+ parser (>= 3.3.7.2)
201
+ prism (~> 1.4)
202
+ rubocop-performance (1.25.0)
203
+ lint_roller (~> 1.1)
204
+ rubocop (>= 1.75.0, < 2.0)
205
+ rubocop-ast (>= 1.38.0, < 2.0)
206
+ ruby-progressbar (1.13.0)
207
+ securerandom (0.4.1)
208
+ sqlite3 (2.7.2-x86_64-linux-gnu)
209
+ standard (1.50.0)
210
+ language_server-protocol (~> 3.17.0.2)
211
+ lint_roller (~> 1.0)
212
+ rubocop (~> 1.75.5)
213
+ standard-custom (~> 1.0.0)
214
+ standard-performance (~> 1.8)
215
+ standard-custom (1.0.2)
216
+ lint_roller (~> 1.0)
217
+ rubocop (~> 1.50)
218
+ standard-performance (1.8.0)
219
+ lint_roller (~> 1.1)
220
+ rubocop-performance (~> 1.25.0)
221
+ stringio (3.1.7)
222
+ thor (1.4.0)
223
+ timeout (0.4.3)
224
+ tzinfo (2.0.6)
225
+ concurrent-ruby (~> 1.0)
226
+ unicode-display_width (3.1.4)
227
+ unicode-emoji (~> 4.0, >= 4.0.4)
228
+ unicode-emoji (4.0.4)
229
+ useragent (0.16.11)
230
+ websocket-driver (0.8.0)
231
+ base64
232
+ websocket-extensions (>= 0.1.0)
233
+ websocket-extensions (0.1.5)
234
+ zeitwerk (2.6.18)
235
+
236
+ PLATFORMS
237
+ x86_64-linux
238
+
239
+ DEPENDENCIES
240
+ arerd!
241
+ irb
242
+ minitest
243
+ rake
244
+ sqlite3
245
+ standard
246
+
247
+ BUNDLED WITH
248
+ 2.3.27
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Shuhei YOSHIDA
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # Arerd
2
+
3
+ **Arerd** is a Ruby gem that extracts Entity-Relationship (ER) information from your ActiveRecord models and generates clear, visual ER diagrams in [Mermaid](https://mermaid-js.github.io/) format.
4
+
5
+ Once integrated into your Rails project, Arerd provides a convenient Rake task (`db:erd:mermaid` or `db:erd:markdown`) that outputs a Mermaid-formatted ER diagram directly to your terminal.
6
+
7
+ You can also automate ER diagram generation in your CI pipeline by outputting the diagram in Markdown format. This ensures your ER diagram documentation is always up to date and easy to maintain.
8
+
9
+ ## Installation
10
+
11
+ Add Arerd to your Rails application's Gemfile:
12
+
13
+ ```ruby
14
+ gem "arerd"
15
+ ```
16
+
17
+ Then install the gem:
18
+
19
+ ```shell
20
+ bundle install
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### Generate a Mermaid ER Diagram
26
+
27
+ Run the following command to create a Mermaid ER diagram:
28
+
29
+ ```shell
30
+ bin/rails db:erd:mermaid
31
+ ```
32
+
33
+ This will output the diagram in Mermaid's `erDiagram` format to standard output. You can copy and paste the result into the [Mermaid Live Editor](https://mermaid.live/) or any Mermaid-compatible tool to visualize your ER diagram.
34
+
35
+ ### Generate a Markdown ER Diagram
36
+
37
+ To output the ER diagram in Markdown format:
38
+
39
+ ```shell
40
+ bin/rails db:erd:markdown
41
+ ```
42
+
43
+ This command prints the diagram wrapped in triple backticks and tagged as `mermaid`, allowing you to preview it directly in supported Markdown editors or viewers.
44
+
45
+ ### Internationalization (I18n) Support
46
+
47
+ Table and column names in diagrams are automatically translated using your Rails application's locale files. Arerd leverages Rails' I18n system to provide localized names for entities and attributes, making diagrams more accessible for international teams.
48
+
49
+ ### Notes
50
+
51
+ * **All associations must specify the `inverse_of` option.**
52
+ * Associations using `has_many :through` are ignored.
53
+ * Polymorphic associations (e.g., `belongs_to :taggable, polymorphic: true`) are not supported.
54
+
55
+ ### Example Output
56
+
57
+ ```mermaid
58
+ erDiagram
59
+ User["User (ユーザー)"] {
60
+ integer id PK "Id"
61
+ string name UK "名前 (indexed)"
62
+ datetime created_at "作成日時"
63
+ datetime updated_at "更新日時"
64
+ }
65
+ Profile["Profile (プロフィール)"] {
66
+ integer id PK "Id"
67
+ integer user_id FK "ユーザーID (indexed)"
68
+ string bio "自己紹介 (nullable)"
69
+ datetime created_at "作成日時"
70
+ datetime updated_at "更新日時"
71
+ }
72
+ Post["Post (投稿)"] {
73
+ integer id PK "Id"
74
+ string title "タイトル"
75
+ text body "本文"
76
+ integer user_id FK "ユーザーID (indexed)"
77
+ datetime created_at "作成日時"
78
+ datetime updated_at "更新日時"
79
+ }
80
+ Notification["Notification (通知)"] {
81
+ integer id PK "Id"
82
+ integer user_id FK "ユーザーID (indexed)"
83
+ integer sender_id FK "送信者ID (nullable, indexed)"
84
+ string message "メッセージ"
85
+ datetime created_at "作成日時"
86
+ datetime updated_at "更新日時"
87
+ }
88
+ Follow["Follow (フォロー)"] {
89
+ integer id PK "Id"
90
+ integer follower_id FK "フォロワーID (indexed)"
91
+ integer followed_id UK "フォロー対象ID (indexed)"
92
+ datetime created_at "作成日時"
93
+ datetime updated_at "更新日時"
94
+ }
95
+ Community["Community (コミュニティ)"] {
96
+ integer id PK "Id"
97
+ string name "名前"
98
+ datetime created_at "作成日時"
99
+ datetime updated_at "更新日時"
100
+ }
101
+
102
+ User ||--o{ Post : "posts / user"
103
+ User ||--o| Profile : "profile / user"
104
+ User ||--o{ Follow : "follows / follower"
105
+ User ||--o{ User : "followees / followers"
106
+ User ||--o{ Follow : "reverse_follows / followee"
107
+ User ||--o{ User : "followers / followees"
108
+ User ||--o{ Notification : "notifications / user"
109
+ User |o--o{ Notification : "sent_notifications / sender"
110
+ Community }o--o{ User : "users / communities"
111
+ ```
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[test standard]
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arerd
4
+ class Association
5
+ attr_reader :left_model, :left_key, :left_association_name,
6
+ :right_model, :right_key, :right_association_name,
7
+ :left_side_multiplicity, :right_side_multiplicity
8
+
9
+ def initialize(
10
+ left_model:,
11
+ left_key:,
12
+ left_association_name:,
13
+ right_model:,
14
+ right_key:,
15
+ right_association_name:,
16
+ left_side_multiplicity:,
17
+ right_side_multiplicity:
18
+ )
19
+ @left_model = left_model
20
+ @left_key = left_key
21
+ @left_association_name = left_association_name
22
+ @right_model = right_model
23
+ @right_key = right_key
24
+ @right_association_name = right_association_name
25
+ @left_side_multiplicity = left_side_multiplicity
26
+ @right_side_multiplicity = right_side_multiplicity
27
+ end
28
+
29
+ def self.build(association)
30
+ case association.macro
31
+ when :has_many
32
+ return if association.options[:through]
33
+
34
+ if association.inverse_of.nil?
35
+ warn "Association #{association.name} has no inverse association. Skipping association."
36
+
37
+ return
38
+ end
39
+
40
+ new(
41
+ left_model: association.active_record,
42
+ left_key: association.active_record_primary_key,
43
+ left_association_name: association.name,
44
+ right_model: association.class_name.constantize,
45
+ right_key: association.foreign_key,
46
+ right_association_name: association.inverse_of.name,
47
+ left_side_multiplicity: association.inverse_of.options[:optional] ? :optional_one : :one,
48
+ right_side_multiplicity: :optional_many
49
+ )
50
+ when :has_one
51
+ return if association.options[:through]
52
+
53
+ if association.inverse_of.nil?
54
+ warn "Association #{association.name} has no inverse association. Skipping association."
55
+
56
+ return
57
+ end
58
+
59
+ new(
60
+ left_model: association.active_record,
61
+ left_key: association.active_record_primary_key,
62
+ left_association_name: association.name,
63
+ right_model: association.class_name.constantize,
64
+ right_key: association.foreign_key,
65
+ right_association_name: association.inverse_of.name,
66
+ left_side_multiplicity: association.inverse_of.options[:optional] ? :optional_one : :one,
67
+ right_side_multiplicity: :optional_one
68
+ )
69
+ when :has_and_belongs_to_many
70
+ if association.inverse_of.nil?
71
+ warn "Association #{association.name} has no inverse association. Skipping association."
72
+
73
+ return
74
+ end
75
+
76
+ return if association.active_record.name > association.class_name # Avoid duplicate associations
77
+
78
+ new(
79
+ left_model: association.active_record,
80
+ left_key: association.active_record_primary_key,
81
+ left_association_name: association.name,
82
+ right_model: association.class_name.constantize,
83
+ right_key: association.foreign_key,
84
+ right_association_name: association.inverse_of.name,
85
+ left_side_multiplicity: :optional_many,
86
+ right_side_multiplicity: :optional_many
87
+ )
88
+ when :belongs_to
89
+ if association.options[:polymorphic]
90
+ warn "Polymorphic associations are not supported yet. Skipping association #{association.name}."
91
+
92
+ return
93
+ end
94
+
95
+ if association.inverse_of.nil?
96
+ warn "Association #{association.name} has no inverse association."
97
+ end
98
+ else
99
+ warn "Unknown association type: #{association.macro} for #{association.name} in #{left_model.name}"
100
+ end
101
+ end
102
+
103
+ def self.build_associations_from_models(models)
104
+ models.flat_map(&:reflect_on_all_associations).map { |association|
105
+ build(association)
106
+ }.compact
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module Arerd
6
+ class ErdGenerator
7
+ MERMAID_TEMPLATE_PATH = File.expand_path("./templates/erd.mmd.erb", __dir__)
8
+ MARKDOWN_TEMPLATE_PATH = File.expand_path("./templates/erd.md.erb", __dir__)
9
+
10
+ def self.generate_markdown(models:, associations:)
11
+ mermaid = generate_mermaid(models:, associations:)
12
+
13
+ template = File.read(MARKDOWN_TEMPLATE_PATH)
14
+ ERB.new(template, trim_mode: "-").result_with_hash(mermaid:)
15
+ end
16
+
17
+ def self.generate_mermaid(models:, associations:)
18
+ template = File.read(MERMAID_TEMPLATE_PATH)
19
+ ERB.new(template, trim_mode: "-").result_with_hash(models:, associations:)
20
+ end
21
+
22
+ def self.collect_models_and_associations
23
+ Rails.application.eager_load!
24
+
25
+ models = ApplicationRecord.descendants
26
+
27
+ associations = Arerd::Association.build_associations_from_models(models)
28
+
29
+ {models:, associations:}
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arerd
4
+ class Railtie < ::Rails::Railtie
5
+ rake_tasks do
6
+ load "arerd/tasks/erd.rake"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "arerd/erd_generator"
4
+
5
+ namespace :db do
6
+ desc "Output ERD as Mermaid to STDOUT"
7
+ namespace :erd do
8
+ task mermaid: :environment do
9
+ models, associations = Arerd::ErdGenerator.collect_models_and_associations.values_at(:models, :associations)
10
+ puts Arerd::ErdGenerator.generate_mermaid(models:, associations:)
11
+ end
12
+
13
+ task markdown: :environment do
14
+ models, associations = Arerd::ErdGenerator.collect_models_and_associations.values_at(:models, :associations)
15
+ puts Arerd::ErdGenerator.generate_markdown(models:, associations:)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ ```mermaid
2
+ <%= mermaid -%>
3
+ ```
@@ -0,0 +1,43 @@
1
+ erDiagram
2
+ <%- models.each do |model| -%>
3
+ <%- foreign_keys = model.reflect_on_all_associations.map(&:foreign_key).uniq -%>
4
+ <%- indexes = (model.connection.indexes(model.table_name) rescue []) -%>
5
+ <%- unique_columns = (indexes.select(&:unique).flat_map(&:columns) rescue []) -%>
6
+ <%= model.name %>["<%= [model.name, "(#{model.model_name.human})"].compact.join(" ") %>"] {
7
+ <%- model.columns.each do |column| -%>
8
+ <%= [
9
+ column.type,
10
+ column.name,
11
+ if model.primary_key == column.name
12
+ "PK"
13
+ elsif foreign_keys.include?(column.name)
14
+ "FK"
15
+ elsif unique_columns.include?(column.name)
16
+ "UK"
17
+ end,
18
+ "\"#{
19
+ model.human_attribute_name(column.name)
20
+ }#{
21
+ [
22
+ column.null ? "nullable" : nil,
23
+ (indexes.any? { |idx| idx.columns.include?(column.name) } ? "indexed" : nil),
24
+ ].compact.join(", ").then { |c| c == "" ? "" : " (#{c})" }
25
+ }\"",
26
+ ].compact.join(" ") %>
27
+ <%- end -%>
28
+ }
29
+ <%- end -%>
30
+
31
+ <%- associations.each do |association| -%>
32
+ <%=
33
+ association.left_model.name
34
+ %> <%=
35
+ { one: "||", many: "}o", optional_one: "|o", optional_many: "}o" }[association.left_side_multiplicity]
36
+ %>--<%=
37
+ { one: "||", many: "o{", optional_one: "o|", optional_many: "o{" }[association.right_side_multiplicity]
38
+ %> <%=
39
+ association.right_model.name
40
+ %> : "<%=
41
+ "#{association.left_association_name} / #{association.right_association_name}"
42
+ %>"
43
+ <%- end -%>
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arerd
4
+ VERSION = "0.1.0"
5
+ end
data/lib/arerd.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "arerd/association"
4
+ require_relative "arerd/erd_generator"
5
+ require_relative "arerd/version"
6
+
7
+ module Arerd
8
+ class Error < StandardError; end
9
+ end
10
+
11
+ require "arerd/railtie" if defined?(Rails::Railtie)
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: arerd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shuhei YOSHIDA
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-07-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Provides a Rake task (db:erd) that extracts Entity-Relationship information
42
+ from ActiveRecord and outputs an E-R diagram in Mermaid notation.
43
+ email:
44
+ - contact@yantene.net
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".devcontainer/Dockerfile"
50
+ - ".devcontainer/compose.yaml"
51
+ - ".devcontainer/devcontainer.json"
52
+ - ".devcontainer/fish/conf.d/abbrs.fish"
53
+ - ".devcontainer/fish/functions/git.fish"
54
+ - ".devcontainer/post_create.fish"
55
+ - ".editorconfig"
56
+ - ".standard.yml"
57
+ - ".tool-versions"
58
+ - Gemfile
59
+ - Gemfile.lock
60
+ - LICENSE
61
+ - README.md
62
+ - Rakefile
63
+ - lib/arerd.rb
64
+ - lib/arerd/association.rb
65
+ - lib/arerd/erd_generator.rb
66
+ - lib/arerd/railtie.rb
67
+ - lib/arerd/tasks/erd.rake
68
+ - lib/arerd/templates/erd.md.erb
69
+ - lib/arerd/templates/erd.mmd.erb
70
+ - lib/arerd/version.rb
71
+ homepage: https://github.com/yantene/arerd
72
+ licenses: []
73
+ metadata:
74
+ allowed_push_host: https://rubygems.org
75
+ homepage_uri: https://github.com/yantene/arerd
76
+ source_code_uri: https://github.com/yantene/arerd
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 3.1.0
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.3.27
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Rails gem for generating ERD from ActiveRecord in Mermaid format
96
+ test_files: []