erbee 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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/README.md +471 -0
- data/Rakefile +12 -0
- data/exe/erbee +93 -0
- data/lib/erbee/association_explorer.rb +60 -0
- data/lib/erbee/cli.rb +13 -0
- data/lib/erbee/configuration.rb +11 -0
- data/lib/erbee/diagram_renderer.rb +133 -0
- data/lib/erbee/model_info.rb +9 -0
- data/lib/erbee/version.rb +5 -0
- data/lib/erbee.rb +23 -0
- data/sig/erbee.rbs +4 -0
- metadata +101 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4058446709d36b729b7771aa2d261686e19586816fcac65c6de80d649c3df87f
|
|
4
|
+
data.tar.gz: ba2528945f35dbb48d73ff594aea3d77f9f94d04cd55b85d738d66bbf789ea4e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 79d37934763e32e268afb17ae0469238e968b50bd9616521d64929e3c3813de7abdcc113e338be108c6ba3794b1ee7e244013510ff5d10e9d733a8aa129f7cd9
|
|
7
|
+
data.tar.gz: 18191084b2c14e416e5eeccb37b3880ec6699fe6dfa62d9736d4eb763bcc8cb3541b606c3a687a7fd4505dba55282cdc4c3f2b971b8bdddacbb3799c9dd2707c
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
# Erbee
|
|
2
|
+
|
|
3
|
+
Erbee is a Ruby gem designed to automatically generate Entity-Relationship (ER) diagrams for your Rails applications. Leveraging the power of [Mermaid](https://mermaid-js.github.io/) for visualization, Erbee provides an easy and flexible way to visualize your database schema and model associations directly from your Rails models.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Features](#features)
|
|
8
|
+
- [Installation](#installation)
|
|
9
|
+
- [Configuration](#configuration)
|
|
10
|
+
- [Usage](#usage)
|
|
11
|
+
- [Generating ER Diagrams](#generating-er-diagrams)
|
|
12
|
+
- [Output Options](#output-options)
|
|
13
|
+
- [Testing](#testing)
|
|
14
|
+
- [Local File Output Test](#local-file-output-test)
|
|
15
|
+
- [Tempfile Output Test](#tempfile-output-test)
|
|
16
|
+
- [Examples](#examples)
|
|
17
|
+
- [Sample ER Diagram](#sample-er-diagram)
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Automatic ER Diagram Generation**: Generate ER diagrams based on your Rails models and their associations.
|
|
22
|
+
- **Mermaid Integration**: Utilize Mermaid's `erDiagram` syntax for clear and interactive diagrams.
|
|
23
|
+
- **Flexible Output Options**: Output diagrams to local Markdown files or temporary files for testing purposes.
|
|
24
|
+
- **Customizable Configuration**: Easily configure output paths and generation settings.
|
|
25
|
+
- **Comprehensive Testing**: Includes tests for generating large ER diagrams with up to 50 tables and random relationships.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Add Erbee to your application's Gemfile:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
gem 'erbee'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
And then execute:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bundle install
|
|
39
|
+
```
|
|
40
|
+
Or install it yourself as:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
gem install erbee
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
Erbee can be configured to specify output paths and other settings. You can configure Erbee in an initializer or directly within your Ruby scripts.
|
|
48
|
+
|
|
49
|
+
### Example Configuration
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
Erbee.configure do |config|
|
|
53
|
+
config.output_path = "path/to/your/erdiagram.md" # Specify your desired output path
|
|
54
|
+
config.depth = 2 # Specify the depth for association exploration (optional)
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
Erbee provides a straightforward interface to generate ER diagrams from your Rails models.
|
|
60
|
+
|
|
61
|
+
### Generating ER Diagrams
|
|
62
|
+
You can generate an ER diagram by invoking the `DiagramRenderer` with your model information.
|
|
63
|
+
|
|
64
|
+
#### Example:
|
|
65
|
+
```ruby
|
|
66
|
+
# lib/erbee/diagram_renderer.rb
|
|
67
|
+
require 'erbee/model_info'
|
|
68
|
+
require 'ostruct'
|
|
69
|
+
|
|
70
|
+
module Erbee
|
|
71
|
+
class DiagramRenderer
|
|
72
|
+
def initialize(model_infos)
|
|
73
|
+
@model_infos = model_infos
|
|
74
|
+
@output_path = Erbee.configuration.output_path
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def render
|
|
78
|
+
mermaid_code = build_mermaid_er
|
|
79
|
+
File.write(@output_path, mermaid_code)
|
|
80
|
+
puts "Generated ER diagram (Mermaid erDiagram) at: #{@output_path}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Generates Mermaid erDiagram syntax
|
|
86
|
+
def build_mermaid_er
|
|
87
|
+
lines = []
|
|
88
|
+
lines << "```mermaid"
|
|
89
|
+
lines << "erDiagram"
|
|
90
|
+
|
|
91
|
+
# Table Definitions
|
|
92
|
+
@model_infos.each do |model_info|
|
|
93
|
+
lines << build_table_definition(model_info)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Relationships
|
|
97
|
+
visited_edges = {}
|
|
98
|
+
@model_infos.each do |model_info|
|
|
99
|
+
model_info.associations.each do |assoc|
|
|
100
|
+
src_table = model_info.model_class.table_name
|
|
101
|
+
dst_info = @model_infos.find { |mi| mi.model_class.name == assoc[:class_name] }
|
|
102
|
+
next unless dst_info
|
|
103
|
+
|
|
104
|
+
dst_table = dst_info.model_class.table_name
|
|
105
|
+
|
|
106
|
+
edge_key = [src_table, dst_table].sort.join("-")
|
|
107
|
+
next if visited_edges[edge_key]
|
|
108
|
+
|
|
109
|
+
visited_edges[edge_key] = true
|
|
110
|
+
|
|
111
|
+
relation_str = mermaid_relation(assoc[:type], src_table, dst_table)
|
|
112
|
+
lines << " #{relation_str}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
lines << "```"
|
|
117
|
+
lines.join("\n")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Builds table definition in Mermaid syntax
|
|
121
|
+
def build_table_definition(model_info)
|
|
122
|
+
table_name = model_info.model_class.table_name
|
|
123
|
+
columns_def = model_info.columns.map do |col|
|
|
124
|
+
"#{mermaid_type(col[:type])} #{col[:name]}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
definition_lines = []
|
|
128
|
+
definition_lines << " #{table_name} {"
|
|
129
|
+
columns_def.each do |c|
|
|
130
|
+
definition_lines << " #{c}"
|
|
131
|
+
end
|
|
132
|
+
definition_lines << " }"
|
|
133
|
+
definition_lines.join("\n")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Defines relationships based on association type
|
|
137
|
+
def mermaid_relation(assoc_type, src_table, dst_table)
|
|
138
|
+
case assoc_type
|
|
139
|
+
when :belongs_to
|
|
140
|
+
"#{src_table} |{--|| #{dst_table} : \"N:1\""
|
|
141
|
+
when :has_many
|
|
142
|
+
"#{src_table} ||--|{ #{dst_table} : \"1:N\""
|
|
143
|
+
when :has_one
|
|
144
|
+
"#{src_table} ||--|| #{dst_table} : \"1:1\""
|
|
145
|
+
when :has_and_belongs_to_many
|
|
146
|
+
"#{src_table} }|--|{ #{dst_table} : \"N:N\""
|
|
147
|
+
else
|
|
148
|
+
"#{src_table} ||--|| #{dst_table} : \"1:1\""
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Converts Rails column types to Mermaid types
|
|
153
|
+
def mermaid_type(col_type)
|
|
154
|
+
case col_type
|
|
155
|
+
when :integer
|
|
156
|
+
"int"
|
|
157
|
+
when :float, :decimal
|
|
158
|
+
"float"
|
|
159
|
+
when :datetime, :date, :time, :timestamp
|
|
160
|
+
"datetime"
|
|
161
|
+
when :boolean
|
|
162
|
+
"boolean"
|
|
163
|
+
else
|
|
164
|
+
"string"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Output Options
|
|
172
|
+
Erbee allows you to output the generated ER diagram to either a local Markdown file or a temporary file for testing.
|
|
173
|
+
|
|
174
|
+
#### Local File Output
|
|
175
|
+
Configure Erbee to output to a specific file path:
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
Erbee.configure do |config|
|
|
179
|
+
config.output_path = "spec/output/erdiagram.md"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
model_infos = Erbee::AssociationExplorer.new("User", depth: 2).explore
|
|
183
|
+
renderer = Erbee::DiagramRenderer.new(model_infos)
|
|
184
|
+
renderer.render
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### Temporary File Output (For Testing)
|
|
188
|
+
For testing purposes, you can use Ruby's Tempfile to generate a temporary file that gets deleted after use.
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
require "tempfile"
|
|
192
|
+
|
|
193
|
+
tmpfile = Tempfile.new(["erdiagram", ".md"])
|
|
194
|
+
Erbee.configure do |config|
|
|
195
|
+
config.output_path = tmpfile.path
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
model_infos = Erbee::AssociationExplorer.new("User", depth: 2).explore
|
|
199
|
+
renderer = Erbee::DiagramRenderer.new(model_infos)
|
|
200
|
+
renderer.render
|
|
201
|
+
|
|
202
|
+
# After testing
|
|
203
|
+
tmpfile.close
|
|
204
|
+
tmpfile.unlink
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Testing
|
|
208
|
+
Erbee includes comprehensive tests to ensure the reliability of ER diagram generation, even with large and complex schemas.
|
|
209
|
+
|
|
210
|
+
#### Setting Up Tests
|
|
211
|
+
Ensure that you have RSpec installed and properly configured in your project. Erbee's tests include helpers for generating random model data.
|
|
212
|
+
|
|
213
|
+
##### Random Model Info Helper
|
|
214
|
+
Create a helper to generate random model information for testing.
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
# spec/support/random_model_info_helper.rb
|
|
218
|
+
require 'ostruct'
|
|
219
|
+
|
|
220
|
+
module RandomModelInfoHelper
|
|
221
|
+
# Generates an array of random ModelInfo objects with consistent associations
|
|
222
|
+
def create_random_model_infos(table_count = 50, seed = 12345)
|
|
223
|
+
# Fix the random seed for reproducibility
|
|
224
|
+
srand seed
|
|
225
|
+
|
|
226
|
+
# Generate table names: table01, table02, ..., table50
|
|
227
|
+
table_names = (1..table_count).map { |i| "table%02d" % i }
|
|
228
|
+
|
|
229
|
+
# Initialize ModelInfo objects with random columns
|
|
230
|
+
model_infos = table_names.map do |tname|
|
|
231
|
+
# Randomly assign 2 to 5 columns
|
|
232
|
+
col_count = rand(2..5)
|
|
233
|
+
columns = col_count.times.map do |i|
|
|
234
|
+
{
|
|
235
|
+
name: "col#{('A'.ord + i).chr}", # colA, colB, etc.
|
|
236
|
+
type: random_type, # :integer, :string, etc.
|
|
237
|
+
null: [true, false].sample
|
|
238
|
+
}
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Initialize associations empty, to be filled later
|
|
242
|
+
associations = []
|
|
243
|
+
|
|
244
|
+
# Create a fake model_class object with name and table_name
|
|
245
|
+
fake_class = OpenStruct.new(
|
|
246
|
+
name: tname.capitalize, # e.g., "Table01"
|
|
247
|
+
table_name: tname # e.g., "table01"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
Erbee::ModelInfo.new(
|
|
251
|
+
model_class: fake_class,
|
|
252
|
+
associations: associations,
|
|
253
|
+
columns: columns
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Assign consistent associations
|
|
258
|
+
model_infos.each do |mi|
|
|
259
|
+
rand(0..3).times do
|
|
260
|
+
assoc_type = random_assoc_type
|
|
261
|
+
target = (model_infos - [mi]).sample
|
|
262
|
+
next if target.nil?
|
|
263
|
+
|
|
264
|
+
# Avoid duplicate associations
|
|
265
|
+
existing_assoc = mi.associations.find { |a| a[:class_name] == target.model_class.name }
|
|
266
|
+
next if existing_assoc
|
|
267
|
+
|
|
268
|
+
case assoc_type
|
|
269
|
+
when :belongs_to
|
|
270
|
+
# Current model belongs_to target
|
|
271
|
+
mi.associations << {
|
|
272
|
+
name: "#{target.model_class.table_name}_ref".to_sym,
|
|
273
|
+
type: :belongs_to,
|
|
274
|
+
class_name: target.model_class.name
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# Ensure target has has_many for current model
|
|
278
|
+
target.associations << {
|
|
279
|
+
name: "#{mi.model_class.table_name}_collection".to_sym,
|
|
280
|
+
type: :has_many,
|
|
281
|
+
class_name: mi.model_class.name
|
|
282
|
+
}
|
|
283
|
+
when :has_one
|
|
284
|
+
# Current model has_one target
|
|
285
|
+
mi.associations << {
|
|
286
|
+
name: "#{target.model_class.table_name}_ref".to_sym,
|
|
287
|
+
type: :has_one,
|
|
288
|
+
class_name: target.model_class.name
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# Ensure target has belongs_to for current model
|
|
292
|
+
target.associations << {
|
|
293
|
+
name: "#{mi.model_class.table_name}_ref".to_sym,
|
|
294
|
+
type: :belongs_to,
|
|
295
|
+
class_name: mi.model_class.name
|
|
296
|
+
}
|
|
297
|
+
when :has_many, :has_and_belongs_to_many
|
|
298
|
+
# Current model has_many or has_and_belongs_to_many target
|
|
299
|
+
mi.associations << {
|
|
300
|
+
name: "#{target.model_class.table_name}_collection".to_sym,
|
|
301
|
+
type: assoc_type,
|
|
302
|
+
class_name: target.model_class.name
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Ensure target has belongs_to or has_and_belongs_to_many for current model
|
|
306
|
+
inverse_type = assoc_type == :has_many ? :belongs_to : :has_and_belongs_to_many
|
|
307
|
+
target.associations << {
|
|
308
|
+
name: "#{mi.model_class.table_name}_ref".to_sym,
|
|
309
|
+
type: inverse_type,
|
|
310
|
+
class_name: mi.model_class.name
|
|
311
|
+
}
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
model_infos
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
private
|
|
320
|
+
|
|
321
|
+
# Returns a random column type
|
|
322
|
+
def random_type
|
|
323
|
+
[:integer, :string, :datetime, :boolean].sample
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Returns a random association type
|
|
327
|
+
def random_assoc_type
|
|
328
|
+
[:belongs_to, :has_many, :has_one, :has_and_belongs_to_many].sample
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
#### RSpec Configuration
|
|
334
|
+
Ensure that the spec/support directory is loaded by RSpec by adding the following to your spec/spec_helper.rb or spec/rails_helper.rb:
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
# spec/spec_helper.rb or spec/rails_helper.rb
|
|
338
|
+
|
|
339
|
+
RSpec.configure do |config|
|
|
340
|
+
# Load support files
|
|
341
|
+
Dir[File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f }
|
|
342
|
+
|
|
343
|
+
# Other configurations...
|
|
344
|
+
end
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Local File Output Test
|
|
348
|
+
This test generates a Mermaid erDiagram with 50 random tables and relationships, saving the output to a local file for manual inspection.
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
# spec/erbee/diagram_renderer_large_spec.rb
|
|
352
|
+
require "spec_helper"
|
|
353
|
+
require "fileutils"
|
|
354
|
+
|
|
355
|
+
RSpec.describe Erbee::DiagramRenderer do
|
|
356
|
+
include RandomModelInfoHelper
|
|
357
|
+
|
|
358
|
+
it "generates a Mermaid erDiagram for 50 random tables, saved locally" do
|
|
359
|
+
# 1. Generate random ModelInfo array with fixed seed
|
|
360
|
+
model_infos = create_random_model_infos(50, seed = 12345)
|
|
361
|
+
|
|
362
|
+
# 2. Define output path (e.g., spec/output/random_erdiagram.md)
|
|
363
|
+
output_dir = "spec/output"
|
|
364
|
+
FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
|
|
365
|
+
local_md_path = File.join(output_dir, "random_erdiagram.md")
|
|
366
|
+
|
|
367
|
+
# 3. Configure Erbee to output to the local file
|
|
368
|
+
Erbee.configure do |config|
|
|
369
|
+
config.output_path = local_md_path
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# 4. Render the diagram
|
|
373
|
+
renderer = Erbee::DiagramRenderer.new(model_infos)
|
|
374
|
+
renderer.render
|
|
375
|
+
|
|
376
|
+
# 5. Assertions
|
|
377
|
+
expect(File).to exist(local_md_path)
|
|
378
|
+
content = File.read(local_md_path)
|
|
379
|
+
|
|
380
|
+
# Check that the content includes the Mermaid code block and 'erDiagram'
|
|
381
|
+
expect(content).to include("```mermaid")
|
|
382
|
+
expect(content).to include("erDiagram")
|
|
383
|
+
|
|
384
|
+
# Check that some table definitions are present
|
|
385
|
+
expect(content).to include("table01 {")
|
|
386
|
+
expect(content).to include("table50 {")
|
|
387
|
+
|
|
388
|
+
# Check that relationships are defined
|
|
389
|
+
# Example: "table01 ||--|{ table02 : "1:N""
|
|
390
|
+
expect(content).to match(/table\d{2}\s+\|\|--\|\{\s+table\d{2}/)
|
|
391
|
+
|
|
392
|
+
# Output the path for manual inspection
|
|
393
|
+
puts "Generated ER diagram saved at: #{local_md_path}"
|
|
394
|
+
|
|
395
|
+
# Do not delete the file for manual inspection
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Tempfile Output Test
|
|
401
|
+
This test generates a Mermaid erDiagram with 50 random tables and relationships, saving the output to a temporary file that gets deleted after the test.
|
|
402
|
+
|
|
403
|
+
```ruby
|
|
404
|
+
# spec/erbee/diagram_renderer_tempfile_spec.rb
|
|
405
|
+
require "spec_helper"
|
|
406
|
+
require "tempfile"
|
|
407
|
+
|
|
408
|
+
RSpec.describe Erbee::DiagramRenderer do
|
|
409
|
+
include RandomModelInfoHelper
|
|
410
|
+
|
|
411
|
+
it "generates a Mermaid erDiagram for 50 random tables using Tempfile and deletes it after" do
|
|
412
|
+
# 1. Generate random ModelInfo array with fixed seed
|
|
413
|
+
model_infos = create_random_model_infos(50, seed = 12345)
|
|
414
|
+
|
|
415
|
+
# 2. Create a Tempfile for output
|
|
416
|
+
tmpfile = Tempfile.new(["random_erdiagram", ".md"])
|
|
417
|
+
|
|
418
|
+
# 3. Configure Erbee to output to the Tempfile
|
|
419
|
+
Erbee.configure do |config|
|
|
420
|
+
config.output_path = tmpfile.path
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# 4. Render the diagram
|
|
424
|
+
renderer = Erbee::DiagramRenderer.new(model_infos)
|
|
425
|
+
renderer.render
|
|
426
|
+
|
|
427
|
+
# 5. Assertions
|
|
428
|
+
expect(File).to exist(tmpfile.path)
|
|
429
|
+
content = File.read(tmpfile.path)
|
|
430
|
+
|
|
431
|
+
# Check that the content includes the Mermaid code block and 'erDiagram'
|
|
432
|
+
expect(content).to include("```mermaid")
|
|
433
|
+
expect(content).to include("erDiagram")
|
|
434
|
+
|
|
435
|
+
# Check that some table definitions are present
|
|
436
|
+
expect(content).to include("table01 {")
|
|
437
|
+
expect(content).to include("table50 {")
|
|
438
|
+
|
|
439
|
+
# Check that relationships are defined
|
|
440
|
+
# Example: "table01 ||--|{ table02 : "1:N""
|
|
441
|
+
expect(content).to match(/table\d{2}\s+\|\|--\|\{\s+table\d{2}/)
|
|
442
|
+
|
|
443
|
+
# Output the path for debugging (file will be deleted)
|
|
444
|
+
puts "Generated ER diagram saved at: #{tmpfile.path}"
|
|
445
|
+
|
|
446
|
+
# 6. Cleanup: close and delete the Tempfile
|
|
447
|
+
tmpfile.close
|
|
448
|
+
tmpfile.unlink # This deletes the file
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
## Examples
|
|
454
|
+
### Sample ER Diagram
|
|
455
|
+
Here's an example of how a generated ER diagram might look in Markdown using Mermaid's erDiagram syntax:
|
|
456
|
+
|
|
457
|
+
```merkdown
|
|
458
|
+
erDiagram
|
|
459
|
+
users {
|
|
460
|
+
int id
|
|
461
|
+
string name
|
|
462
|
+
}
|
|
463
|
+
posts {
|
|
464
|
+
int id
|
|
465
|
+
string title
|
|
466
|
+
int user_id
|
|
467
|
+
}
|
|
468
|
+
users ||--|{ posts : "1:N"
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
When rendered with a Mermaid-compatible viewer, this will display an ER diagram showing a one-to-many relationship between `users` and `posts`.
|
data/Rakefile
ADDED
data/exe/erbee
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "erbee"
|
|
5
|
+
|
|
6
|
+
module Erbee
|
|
7
|
+
class CLI
|
|
8
|
+
def self.start(argv)
|
|
9
|
+
self.load_rails_application
|
|
10
|
+
|
|
11
|
+
puts "Loading Rails application succeeded!"
|
|
12
|
+
|
|
13
|
+
options = {
|
|
14
|
+
depth: 2,
|
|
15
|
+
output_path: "er_diagram.md"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
parser = OptionParser.new do |opts|
|
|
19
|
+
opts.banner = "Usage: erbee MODEL [options]"
|
|
20
|
+
|
|
21
|
+
opts.on("--depth=DEPTH", Integer, "Association exploration depth (default: 2)") do |d|
|
|
22
|
+
options[:depth] = d
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
opts.on("--output=PATH", "Output file path (default: er_diagram.md)") do |o|
|
|
26
|
+
options[:output_path] = o
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
opts.on("-h", "--help", "Prints this help") do
|
|
30
|
+
puts opts
|
|
31
|
+
exit
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
parser.parse!(argv)
|
|
36
|
+
|
|
37
|
+
# The first argument is expected to be the model name
|
|
38
|
+
model_name = argv[0]
|
|
39
|
+
if model_name.nil?
|
|
40
|
+
puts parser
|
|
41
|
+
exit 1
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Configure Erbee using the parsed options
|
|
45
|
+
Erbee.configure do |config|
|
|
46
|
+
config.depth = options[:depth]
|
|
47
|
+
config.output_path = options[:output_path]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
explorer = Erbee::AssociationExplorer.new(model_name, depth: Erbee.configuration.depth)
|
|
51
|
+
model_infos = explorer.explore
|
|
52
|
+
|
|
53
|
+
renderer = Erbee::DiagramRenderer.new(model_infos)
|
|
54
|
+
renderer.render
|
|
55
|
+
|
|
56
|
+
puts "ER diagram generated at #{Erbee.configuration.output_path}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.load_rails_application
|
|
60
|
+
if File.exist?("config/environment.rb")
|
|
61
|
+
begin
|
|
62
|
+
require File.expand_path("config/environment.rb", Dir.pwd)
|
|
63
|
+
|
|
64
|
+
if defined?(Rails)
|
|
65
|
+
Rails.application.eager_load!
|
|
66
|
+
Rails.application.config.eager_load_namespaces.each(&:eager_load!) if Rails.application.config.respond_to?(:eager_load_namespaces)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
rescue LoadError
|
|
70
|
+
puts <<~MSG
|
|
71
|
+
Could not load config/environment.rb. Some models may not be loaded,
|
|
72
|
+
resulting in an incomplete diagram. If you're using ActiveRecord without Rails,
|
|
73
|
+
ensure your models are manually required before running this command.
|
|
74
|
+
MSG
|
|
75
|
+
exit 1
|
|
76
|
+
|
|
77
|
+
rescue TypeError
|
|
78
|
+
puts <<~MSG
|
|
79
|
+
Failed to eager load models. Some classes may remain unloaded,
|
|
80
|
+
leading to an incomplete diagram. Please check your environment setup.
|
|
81
|
+
MSG
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
else
|
|
85
|
+
puts "No Rails application found in the current directory."
|
|
86
|
+
exit 1
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Call the CLI entry point with the arguments provided by the user
|
|
93
|
+
Erbee::CLI.start(ARGV)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require 'active_record'
|
|
2
|
+
|
|
3
|
+
module Erbee
|
|
4
|
+
class AssociationExplorer
|
|
5
|
+
def initialize(model_name, depth: Erbee.configuration.depth)
|
|
6
|
+
@model_name = model_name
|
|
7
|
+
@depth = depth
|
|
8
|
+
@visited = {} # { model_class => current_depth }
|
|
9
|
+
@results = [] # Array of ModelInfo
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def explore
|
|
13
|
+
start_model = model_class_for(@model_name)
|
|
14
|
+
traverse(start_model, 0)
|
|
15
|
+
@results
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def traverse(model_class, current_depth)
|
|
21
|
+
return if current_depth > @depth
|
|
22
|
+
return if @visited.key?(model_class) && @visited[model_class] <= current_depth
|
|
23
|
+
|
|
24
|
+
@visited[model_class] = current_depth
|
|
25
|
+
|
|
26
|
+
model_info = build_model_info(model_class)
|
|
27
|
+
@results << model_info unless @results.any? { |m| m.model_class == model_class }
|
|
28
|
+
|
|
29
|
+
model_class.reflect_on_all_associations.each do |assoc|
|
|
30
|
+
next_model_class = assoc.klass
|
|
31
|
+
traverse(next_model_class, current_depth + 1)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def model_class_for(name)
|
|
36
|
+
name.constantize
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_model_info(model_class)
|
|
40
|
+
associations = model_class.reflect_on_all_associations.map do |assoc|
|
|
41
|
+
{
|
|
42
|
+
name: assoc.name,
|
|
43
|
+
type: assoc.macro, # :belongs_to, :has_many, :has_one, etc.
|
|
44
|
+
class_name: assoc.klass.name
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
columns = model_class.columns.map do |col|
|
|
49
|
+
{ name: col.name, type: col.type, null: col.null }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
ModelInfo.new(
|
|
53
|
+
model_class: model_class,
|
|
54
|
+
associations: associations,
|
|
55
|
+
columns: columns
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
data/lib/erbee/cli.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Erbee
|
|
2
|
+
class CLI
|
|
3
|
+
def self.start(args)
|
|
4
|
+
model_name = args[0] || raise("Please specify a model name")
|
|
5
|
+
explorer = AssociationExplorer.new(model_name)
|
|
6
|
+
model_infos = explorer.explore
|
|
7
|
+
|
|
8
|
+
renderer = DiagramRenderer.new(model_infos)
|
|
9
|
+
renderer.render
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
|
|
3
|
+
module Erbee
|
|
4
|
+
class DiagramRenderer
|
|
5
|
+
def initialize(model_infos)
|
|
6
|
+
@model_infos = model_infos
|
|
7
|
+
@output_path = Erbee.configuration.output_path
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def render
|
|
11
|
+
mermaid_code = build_mermaid_er
|
|
12
|
+
File.write(@output_path, mermaid_code)
|
|
13
|
+
puts "Generated ER diagram (Mermaid erDiagram) at: #{@output_path}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# Generates Mermaid syntax for ER Diagram
|
|
19
|
+
def build_mermaid_er
|
|
20
|
+
lines = []
|
|
21
|
+
# Start of code block
|
|
22
|
+
lines << "```mermaid"
|
|
23
|
+
lines << "erDiagram"
|
|
24
|
+
|
|
25
|
+
# Table definition section (TABLE { ... } )
|
|
26
|
+
@model_infos.each do |model_info|
|
|
27
|
+
lines << build_table_definition(model_info)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Relationships (TABLE1 ||--|{ TABLE2 : "1:N" )
|
|
31
|
+
visited_edges = {}
|
|
32
|
+
@model_infos.each do |model_info|
|
|
33
|
+
model_info.associations.each do |assoc|
|
|
34
|
+
# Source and Destination table names
|
|
35
|
+
src_table = model_info.model_class.table_name
|
|
36
|
+
dst_info = @model_infos.find { |mi| mi.model_class.name == assoc[:class_name] }
|
|
37
|
+
next unless dst_info
|
|
38
|
+
|
|
39
|
+
dst_table = dst_info.model_class.table_name
|
|
40
|
+
|
|
41
|
+
# Do not write duplicate edges that have been used once
|
|
42
|
+
edge_key = [src_table, dst_table].sort.join("-")
|
|
43
|
+
next if visited_edges[edge_key]
|
|
44
|
+
visited_edges[edge_key] = true
|
|
45
|
+
|
|
46
|
+
# Get relation symbol
|
|
47
|
+
relation_str = mermaid_relation(assoc[:type], src_table, dst_table)
|
|
48
|
+
lines << " #{relation_str}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
lines << "```"
|
|
53
|
+
lines.join("\n")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Table definition section
|
|
57
|
+
# Example:
|
|
58
|
+
# TableName {
|
|
59
|
+
# int id
|
|
60
|
+
# string name
|
|
61
|
+
# }
|
|
62
|
+
def build_table_definition(model_info)
|
|
63
|
+
table_name = model_info.model_class.table_name
|
|
64
|
+
columns_def = model_info.columns.map do |col|
|
|
65
|
+
# e.g. "int id", "string name"
|
|
66
|
+
# Example of slightly converting the type to resemble Mermaid:
|
|
67
|
+
"#{mermaid_type(col[:type])} #{col[:name]}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
definition_lines = []
|
|
71
|
+
definition_lines << " #{table_name} {"
|
|
72
|
+
columns_def.each do |c|
|
|
73
|
+
definition_lines << " #{c}"
|
|
74
|
+
end
|
|
75
|
+
definition_lines << " }"
|
|
76
|
+
definition_lines.join("\n")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Outputs symbols like 1:1 / 1:N / N:N based on the type of relationship
|
|
80
|
+
# In Mermaid's erDiagram, you can use symbols like ||--|| for 1:1, ||--|{ for 1:N, }|--|{ for N:N, etc.
|
|
81
|
+
# Example: "TABLE1 ||--|{ TABLE2 : "1:N""
|
|
82
|
+
def mermaid_relation(assoc_type, src_table, dst_table)
|
|
83
|
+
# Example of provisional mapping:
|
|
84
|
+
# belongs_to → 1:1 (??) or "N:1", actual determination is a bit more complex
|
|
85
|
+
# has_many → 1:N
|
|
86
|
+
# has_one → 1:1
|
|
87
|
+
# (many-to-many → has_and_belongs_to_many → N:N)
|
|
88
|
+
# Here, simply:
|
|
89
|
+
# belongs_to: src is N, dst is 1
|
|
90
|
+
# has_many: src is 1, dst is N
|
|
91
|
+
# has_one: src is 1, dst is 1
|
|
92
|
+
# has_and_belongs_to_many: N:N
|
|
93
|
+
# If unknown, treat as 1:1
|
|
94
|
+
case assoc_type
|
|
95
|
+
when :belongs_to
|
|
96
|
+
# If "Post" belongs_to "User", then Post:User = N:1
|
|
97
|
+
# => Post |{--|| User
|
|
98
|
+
# (In Mermaid's erDiagram, "TABLE1 |{--|| TABLE2" means TABLE1:N, TABLE2:1)
|
|
99
|
+
"#{src_table} |{--|| #{dst_table} : \"N:1\""
|
|
100
|
+
when :has_many
|
|
101
|
+
# "User" has_many "Posts" => User:Posts = 1:N
|
|
102
|
+
# => User ||--|{ Post
|
|
103
|
+
"#{src_table} ||--|{ #{dst_table} : \"1:N\""
|
|
104
|
+
when :has_one
|
|
105
|
+
# => 1:1
|
|
106
|
+
"#{src_table} ||--|| #{dst_table} : \"1:1\""
|
|
107
|
+
when :has_and_belongs_to_many
|
|
108
|
+
# => N:N
|
|
109
|
+
"#{src_table} }|--|{ #{dst_table} : \"N:N\""
|
|
110
|
+
else
|
|
111
|
+
# fallback -> 1:1
|
|
112
|
+
"#{src_table} ||--|| #{dst_table} : \"1:1\""
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Simple conversion of column types
|
|
117
|
+
# If you have types like integer/string/datetime in SQLite, you can align with Mermaid's notation
|
|
118
|
+
def mermaid_type(col_type)
|
|
119
|
+
case col_type
|
|
120
|
+
when :integer
|
|
121
|
+
"int"
|
|
122
|
+
when :float, :decimal
|
|
123
|
+
"float"
|
|
124
|
+
when :datetime, :date, :time, :timestamp
|
|
125
|
+
"datetime"
|
|
126
|
+
when :boolean
|
|
127
|
+
"boolean"
|
|
128
|
+
else
|
|
129
|
+
"string"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
data/lib/erbee.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erbee/version"
|
|
4
|
+
require "erbee/configuration"
|
|
5
|
+
require "erbee/association_explorer"
|
|
6
|
+
require "erbee/model_info"
|
|
7
|
+
require "erbee/diagram_renderer"
|
|
8
|
+
require "erbee/cli"
|
|
9
|
+
|
|
10
|
+
module Erbee
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
class << self
|
|
13
|
+
attr_writer :configuration
|
|
14
|
+
|
|
15
|
+
def configuration
|
|
16
|
+
@configuration ||= Configuration.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def configure
|
|
20
|
+
yield(configuration)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/sig/erbee.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: erbee
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- allister0098
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-01-19 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activerecord
|
|
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: rspec
|
|
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
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: sqlite3
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
description: Erbee is a Ruby gem designed to automatically generate Entity-Relationship
|
|
56
|
+
(ER) diagrams for your Rails applications. Leveraging the power of Mermaid for visualization,
|
|
57
|
+
Erbee provides an easy and flexible way to visualize your database schema and model
|
|
58
|
+
associations directly from your Rails models.
|
|
59
|
+
email:
|
|
60
|
+
executables:
|
|
61
|
+
- erbee
|
|
62
|
+
extensions: []
|
|
63
|
+
extra_rdoc_files: []
|
|
64
|
+
files:
|
|
65
|
+
- ".rspec"
|
|
66
|
+
- ".rubocop.yml"
|
|
67
|
+
- README.md
|
|
68
|
+
- Rakefile
|
|
69
|
+
- exe/erbee
|
|
70
|
+
- lib/erbee.rb
|
|
71
|
+
- lib/erbee/association_explorer.rb
|
|
72
|
+
- lib/erbee/cli.rb
|
|
73
|
+
- lib/erbee/configuration.rb
|
|
74
|
+
- lib/erbee/diagram_renderer.rb
|
|
75
|
+
- lib/erbee/model_info.rb
|
|
76
|
+
- lib/erbee/version.rb
|
|
77
|
+
- sig/erbee.rbs
|
|
78
|
+
homepage: https://github.com/allister0098/erbee
|
|
79
|
+
licenses:
|
|
80
|
+
- MIT
|
|
81
|
+
metadata: {}
|
|
82
|
+
post_install_message:
|
|
83
|
+
rdoc_options: []
|
|
84
|
+
require_paths:
|
|
85
|
+
- lib
|
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: 3.0.0
|
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
requirements: []
|
|
97
|
+
rubygems_version: 3.5.11
|
|
98
|
+
signing_key:
|
|
99
|
+
specification_version: 4
|
|
100
|
+
summary: Automatically generate ER diagrams for Rails applications using Mermaid.
|
|
101
|
+
test_files: []
|