erbee 0.1.0 → 0.1.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 +4 -4
- data/README.md +36 -455
- data/lib/erbee/association_explorer.rb +72 -16
- data/lib/erbee/diagram_renderer.rb +64 -59
- data/lib/erbee/polymorphic_collector.rb +25 -0
- data/lib/erbee/polymorphic_registry.rb +31 -0
- data/lib/erbee/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 30b52ebea6705191f221bc4b963b8c8d8a21220edd585baacb302f1d00065bee
|
|
4
|
+
data.tar.gz: 3311df2ced5a719ba38127c2db7f260048b495d7fda5ba406b7f7c07571f329c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68e064938a7724aa8010f2f9a12e77b7a90f655465e3ef8b396c083710f94ac8320a4a385ea8cad4a574ee2034e5fb6a1b748ddb6c2a0fd0dad0e77a2f657597
|
|
7
|
+
data.tar.gz: ed469134ce42651a711e995a6c54982d84b5d8eda44c3ae82c3a1cb0116639c258db0e902b98d1636a91e3af6f1fc43074b02975fc04a2183b834f174151677c
|
data/README.md
CHANGED
|
@@ -1,471 +1,52 @@
|
|
|
1
1
|
# Erbee
|
|
2
2
|
|
|
3
|
-
Erbee is a
|
|
3
|
+
**Erbee** is a simple gem that generates Mermaid-based ER diagrams for your existing Rails or ActiveRecord projects.
|
|
4
|
+
You run a single command, get a `.md` file (in Mermaid format), then convert it to `.svg` via Docker if you wish.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
---
|
|
6
7
|
|
|
7
|
-
|
|
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)
|
|
8
|
+
## 1. Quick Installation & Setup
|
|
18
9
|
|
|
19
|
-
|
|
10
|
+
1. **Add Erbee to your Gemfile** (in your existing project):
|
|
11
|
+
```ruby
|
|
12
|
+
# Gemfile
|
|
13
|
+
gem 'erbee'
|
|
14
|
+
```
|
|
15
|
+
2. **Install the gem**:
|
|
16
|
+
```bash
|
|
17
|
+
bundle install
|
|
18
|
+
```
|
|
19
|
+
3. **Run the CLI (inside your project directory)**:
|
|
20
|
+
```bash
|
|
21
|
+
bundle exec erbee User --depth=1
|
|
22
|
+
```
|
|
23
|
+
- This outputs a Mermaid .md file (e.g. er_diagram.md) with your database entities and associations.
|
|
20
24
|
|
|
21
|
-
|
|
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.
|
|
25
|
+
## 2. Converting Mermaid .md to .svg
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Add Erbee to your application's Gemfile:
|
|
30
|
-
|
|
31
|
-
```ruby
|
|
32
|
-
gem 'erbee'
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
And then execute:
|
|
27
|
+
To view the diagram as an SVG, you can use Docker and the mermaid-cli container:
|
|
36
28
|
|
|
37
29
|
```bash
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
30
|
+
# Example: Using the minlag/mermaid-cli Docker image
|
|
31
|
+
# - Mount the current directory so Mermaid can read/write files
|
|
32
|
+
docker run -it --rm \
|
|
33
|
+
-v "$PWD":/data \
|
|
34
|
+
minlag/mermaid-cli \
|
|
35
|
+
-i er_diagram.md -o er_diagram.svg
|
|
345
36
|
```
|
|
346
37
|
|
|
347
|
-
|
|
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
|
|
38
|
+
Now you have er_diagram.svg in your project folder—open it in any browser or image viewer.
|
|
371
39
|
|
|
372
|
-
|
|
373
|
-
renderer = Erbee::DiagramRenderer.new(model_infos)
|
|
374
|
-
renderer.render
|
|
40
|
+
## 3. View the SVG in Your Browser
|
|
375
41
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
42
|
+
Depending on your OS, run one of these commands:
|
|
43
|
+
- **macOS**:
|
|
44
|
+
```bash
|
|
45
|
+
open er_diagram.svg
|
|
451
46
|
```
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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"
|
|
47
|
+
- **Linux**:
|
|
48
|
+
```bash
|
|
49
|
+
xdg-open er_diagram.svg
|
|
469
50
|
```
|
|
470
51
|
|
|
471
|
-
|
|
52
|
+
After that, your browser (or default viewer) should display the diagram.
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
require
|
|
1
|
+
require "active_record"
|
|
2
|
+
require_relative "polymorphic_collector"
|
|
2
3
|
|
|
3
4
|
module Erbee
|
|
4
5
|
class AssociationExplorer
|
|
5
6
|
def initialize(model_name, depth: Erbee.configuration.depth)
|
|
6
7
|
@model_name = model_name
|
|
7
8
|
@depth = depth
|
|
8
|
-
@visited = {}
|
|
9
|
-
@results = []
|
|
9
|
+
@visited = {}
|
|
10
|
+
@results = []
|
|
11
|
+
# We use an existing PolymorphicCollector to gather reverse-polymorphic associations.
|
|
12
|
+
@polymorphic_registry = PolymorphicCollector.collect!
|
|
10
13
|
end
|
|
11
14
|
|
|
12
15
|
def explore
|
|
@@ -19,16 +22,29 @@ module Erbee
|
|
|
19
22
|
|
|
20
23
|
def traverse(model_class, current_depth)
|
|
21
24
|
return if current_depth > @depth
|
|
25
|
+
# If we've already visited this model at an equal or lower depth, skip.
|
|
22
26
|
return if @visited.key?(model_class) && @visited[model_class] <= current_depth
|
|
23
27
|
|
|
24
28
|
@visited[model_class] = current_depth
|
|
25
29
|
|
|
26
30
|
model_info = build_model_info(model_class)
|
|
27
|
-
|
|
31
|
+
unless @results.any? { |m| m.model_class == model_class }
|
|
32
|
+
@results << model_info
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Use the associations from model_info to recurse further
|
|
36
|
+
model_info.associations.each do |assoc|
|
|
37
|
+
next_class_names = assoc[:class_name].is_a?(Array) ? assoc[:class_name] : [assoc[:class_name]]
|
|
38
|
+
next_class_names.each do |cn|
|
|
39
|
+
next if cn == "POLYMORPHIC"
|
|
28
40
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
begin
|
|
42
|
+
next_model = cn.constantize
|
|
43
|
+
traverse(next_model, current_depth + 1)
|
|
44
|
+
rescue NameError
|
|
45
|
+
# Skip if the class doesn't exist
|
|
46
|
+
end
|
|
47
|
+
end
|
|
32
48
|
end
|
|
33
49
|
end
|
|
34
50
|
|
|
@@ -37,24 +53,64 @@ module Erbee
|
|
|
37
53
|
end
|
|
38
54
|
|
|
39
55
|
def build_model_info(model_class)
|
|
40
|
-
associations = model_class.reflect_on_all_associations.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
associations = model_class.reflect_on_all_associations.flat_map do |assoc|
|
|
57
|
+
# 1) belongs_to with polymorphic: true
|
|
58
|
+
if assoc.macro == :belongs_to && assoc.polymorphic?
|
|
59
|
+
# Example: belongs_to :customer, polymorphic: true
|
|
60
|
+
poly_name = assoc.name.to_s
|
|
61
|
+
# The collector may return multiple owner classes if they all have has_many ... as: poly_name
|
|
62
|
+
possible_owners = @polymorphic_registry.possible_owners(poly_name)
|
|
63
|
+
if possible_owners.empty?
|
|
64
|
+
# Fallback if no owners are found
|
|
65
|
+
[{
|
|
66
|
+
name: assoc.name,
|
|
67
|
+
type: assoc.macro, # e.g., :belongs_to
|
|
68
|
+
polymorphic: true,
|
|
69
|
+
class_name: "POLYMORPHIC"
|
|
70
|
+
}]
|
|
71
|
+
else
|
|
72
|
+
possible_owners.map do |owner_class_name|
|
|
73
|
+
{
|
|
74
|
+
name: assoc.name,
|
|
75
|
+
type: assoc.macro,
|
|
76
|
+
polymorphic: true,
|
|
77
|
+
class_name: owner_class_name
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# 2) has_many (or has_one) with as: :xxx => reverse polymorphic
|
|
83
|
+
elsif %i[has_many has_one].include?(assoc.macro) && assoc.options[:as].present?
|
|
84
|
+
# Example: has_many :images, as: :imageable
|
|
85
|
+
# Rails reflection returns assoc.polymorphic? == false, but we manually set polymorphic: true here
|
|
86
|
+
[{
|
|
87
|
+
name: assoc.name,
|
|
88
|
+
type: assoc.macro, # e.g., :has_many
|
|
89
|
+
polymorphic: true, # Reverse polymorphic side
|
|
90
|
+
class_name: assoc.klass.name
|
|
91
|
+
}]
|
|
92
|
+
|
|
93
|
+
else
|
|
94
|
+
# 3) Normal association
|
|
95
|
+
[{
|
|
96
|
+
name: assoc.name,
|
|
97
|
+
type: assoc.macro, # :belongs_to, :has_many, ...
|
|
98
|
+
polymorphic: false,
|
|
99
|
+
class_name: assoc.klass.name
|
|
100
|
+
}]
|
|
101
|
+
end
|
|
46
102
|
end
|
|
47
103
|
|
|
48
104
|
columns = model_class.columns.map do |col|
|
|
49
105
|
{ name: col.name, type: col.type, null: col.null }
|
|
50
106
|
end
|
|
51
107
|
|
|
108
|
+
# ModelInfo is initialized with keyword arguments
|
|
52
109
|
ModelInfo.new(
|
|
53
|
-
model_class:
|
|
110
|
+
model_class: model_class,
|
|
54
111
|
associations: associations,
|
|
55
|
-
columns:
|
|
112
|
+
columns: columns
|
|
56
113
|
)
|
|
57
114
|
end
|
|
58
115
|
end
|
|
59
116
|
end
|
|
60
|
-
|
|
@@ -27,25 +27,42 @@ module Erbee
|
|
|
27
27
|
lines << build_table_definition(model_info)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
# Relationships
|
|
30
|
+
# Relationships
|
|
31
31
|
visited_edges = {}
|
|
32
32
|
@model_infos.each do |model_info|
|
|
33
33
|
model_info.associations.each do |assoc|
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
dst_info = @model_infos.find { |mi| mi.model_class.name == assoc[:class_name] }
|
|
37
|
-
next unless dst_info
|
|
34
|
+
# Some associations may have an array of class_name if polymorphic => multiple owners
|
|
35
|
+
class_names = assoc[:class_name].is_a?(Array) ? assoc[:class_name] : [assoc[:class_name]]
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
class_names.each do |dst_class_name|
|
|
38
|
+
if dst_class_name == "POLYMORPHIC"
|
|
39
|
+
# We can either skip or draw a dummy node. Example: skip or draw a placeholder
|
|
40
|
+
# Here we demonstrate a dummy edge:
|
|
41
|
+
lines << " #{draw_poly_dummy_edge(model_info, assoc)}"
|
|
42
|
+
next
|
|
43
|
+
end
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
visited_edges[edge_key] = true
|
|
45
|
+
# Find the corresponding model info for dst_class_name
|
|
46
|
+
dst_info = @model_infos.find { |mi| mi.model_class.name == dst_class_name }
|
|
47
|
+
next unless dst_info
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
src_table = model_info.model_class.table_name
|
|
50
|
+
dst_table = dst_info.model_class.table_name
|
|
51
|
+
|
|
52
|
+
# Avoid duplicates
|
|
53
|
+
edge_key = [src_table, dst_table].sort.join("-")
|
|
54
|
+
next if visited_edges[edge_key]
|
|
55
|
+
visited_edges[edge_key] = true
|
|
56
|
+
|
|
57
|
+
# Render the relationship
|
|
58
|
+
relation_str = mermaid_relation(
|
|
59
|
+
assoc[:type], # e.g. :has_many or :belongs_to
|
|
60
|
+
src_table,
|
|
61
|
+
dst_table,
|
|
62
|
+
assoc[:polymorphic] # e.g. true or false
|
|
63
|
+
)
|
|
64
|
+
lines << " #{relation_str}"
|
|
65
|
+
end
|
|
49
66
|
end
|
|
50
67
|
end
|
|
51
68
|
|
|
@@ -53,17 +70,11 @@ module Erbee
|
|
|
53
70
|
lines.join("\n")
|
|
54
71
|
end
|
|
55
72
|
|
|
56
|
-
#
|
|
57
|
-
# Example:
|
|
58
|
-
# TableName {
|
|
59
|
-
# int id
|
|
60
|
-
# string name
|
|
61
|
-
# }
|
|
73
|
+
# Build the table definition in Mermaid syntax
|
|
62
74
|
def build_table_definition(model_info)
|
|
63
75
|
table_name = model_info.model_class.table_name
|
|
64
76
|
columns_def = model_info.columns.map do |col|
|
|
65
|
-
#
|
|
66
|
-
# Example of slightly converting the type to resemble Mermaid:
|
|
77
|
+
# Convert col[:type] to Mermaid-like type
|
|
67
78
|
"#{mermaid_type(col[:type])} #{col[:name]}"
|
|
68
79
|
end
|
|
69
80
|
|
|
@@ -76,44 +87,6 @@ module Erbee
|
|
|
76
87
|
definition_lines.join("\n")
|
|
77
88
|
end
|
|
78
89
|
|
|
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
90
|
# If you have types like integer/string/datetime in SQLite, you can align with Mermaid's notation
|
|
118
91
|
def mermaid_type(col_type)
|
|
119
92
|
case col_type
|
|
@@ -129,5 +102,37 @@ module Erbee
|
|
|
129
102
|
"string"
|
|
130
103
|
end
|
|
131
104
|
end
|
|
105
|
+
|
|
106
|
+
# Provide relationship symbols in Mermaid's erDiagram
|
|
107
|
+
# e.g. belongs_to -> N:1, has_many -> 1:N
|
|
108
|
+
# If is_poly is true, we can optionally alter the notation
|
|
109
|
+
def mermaid_relation(assoc_type, src_table, dst_table, is_poly)
|
|
110
|
+
# assoc_type = :belongs_to / :has_many, is_poly = true/false
|
|
111
|
+
case assoc_type
|
|
112
|
+
when :belongs_to
|
|
113
|
+
if is_poly
|
|
114
|
+
"#{src_table} |{--|| #{dst_table} : \"N:1 (poly)\""
|
|
115
|
+
else
|
|
116
|
+
"#{src_table} |{--|| #{dst_table} : \"N:1\""
|
|
117
|
+
end
|
|
118
|
+
when :has_many
|
|
119
|
+
if is_poly
|
|
120
|
+
"#{src_table} ||--|{ #{dst_table} : \"1:N (poly)\""
|
|
121
|
+
else
|
|
122
|
+
"#{src_table} ||--|{ #{dst_table} : \"1:N\""
|
|
123
|
+
end
|
|
124
|
+
else
|
|
125
|
+
# fallback -> 1:1
|
|
126
|
+
"#{src_table} ||--|| #{dst_table} : \"1:1\""
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Draw a dummy edge if class_name is "POLYMORPHIC"
|
|
131
|
+
def draw_poly_dummy_edge(model_info, assoc)
|
|
132
|
+
src_table = model_info.model_class.table_name
|
|
133
|
+
# e.g. just show it as a special node
|
|
134
|
+
# "image" -- "(poly: imageable)"
|
|
135
|
+
%Q(#{src_table} -- "(poly:#{assoc[:name]})")
|
|
136
|
+
end
|
|
132
137
|
end
|
|
133
138
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require_relative "polymorphic_registry"
|
|
2
|
+
|
|
3
|
+
module Erbee
|
|
4
|
+
class PolymorphicCollector
|
|
5
|
+
def self.collect!
|
|
6
|
+
# After eager_load! is called, we retrieve all models from ActiveRecord::Base.descendants.
|
|
7
|
+
# Then we collect reverse-polymorphic associations (e.g., has_many :images, as: :imageable).
|
|
8
|
+
registry = PolymorphicRegistry.new
|
|
9
|
+
|
|
10
|
+
ActiveRecord::Base.descendants.each do |model|
|
|
11
|
+
model.reflect_on_all_associations.each do |assoc|
|
|
12
|
+
# Check if the macro is :has_many or :has_one and the :as option is present.
|
|
13
|
+
# Example: has_many :images, as: :imageable
|
|
14
|
+
next unless %i[has_many has_one].include?(assoc.macro) && assoc.options[:as].present?
|
|
15
|
+
|
|
16
|
+
polymorphic_name = assoc.options[:as].to_s
|
|
17
|
+
association_name = assoc.name.to_s # e.g., "images"
|
|
18
|
+
registry.add(polymorphic_name, association_name, model.name)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
registry
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Erbee
|
|
2
|
+
class PolymorphicRegistry
|
|
3
|
+
# The data structure is: { polymorphic_name => { association_name => [Array of owner class names] } }
|
|
4
|
+
# For example: { "imageable" => { "images" => ["User", "Article"] } }
|
|
5
|
+
# This holds the models that declare something like 'has_many :images, as: :imageable'.
|
|
6
|
+
|
|
7
|
+
attr_reader :map
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
# Initialize @map so that each key references a nested hash
|
|
11
|
+
@map = Hash.new { |h, k| h[k] = {} }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Register a has_many ... as: :polymorphic_name
|
|
15
|
+
# For example, add("imageable", "images", "User") means:
|
|
16
|
+
# "User" has 'has_many :images, as: :imageable'
|
|
17
|
+
def add(polymorphic_name, association_name, owner_class)
|
|
18
|
+
@map[polymorphic_name][association_name] ||= []
|
|
19
|
+
@map[polymorphic_name][association_name] << owner_class
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# For example, if polymorphic_name = "imageable",
|
|
23
|
+
# possible_owners("imageable") might return ["User", "Article", ...]
|
|
24
|
+
# because each one has 'has_many :images, as: :imageable'.
|
|
25
|
+
def possible_owners(polymorphic_name)
|
|
26
|
+
# We gather all associated class arrays across the nested hash and flatten them:
|
|
27
|
+
# @map["imageable"].values.flatten.uniq
|
|
28
|
+
@map[polymorphic_name].values.flatten.uniq
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/erbee/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: erbee
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- allister0098
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-03-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -56,7 +56,7 @@ description: Erbee is a Ruby gem designed to automatically generate Entity-Relat
|
|
|
56
56
|
(ER) diagrams for your Rails applications. Leveraging the power of Mermaid for visualization,
|
|
57
57
|
Erbee provides an easy and flexible way to visualize your database schema and model
|
|
58
58
|
associations directly from your Rails models.
|
|
59
|
-
email:
|
|
59
|
+
email: taro0098egg@gmail.com
|
|
60
60
|
executables:
|
|
61
61
|
- erbee
|
|
62
62
|
extensions: []
|
|
@@ -73,6 +73,8 @@ files:
|
|
|
73
73
|
- lib/erbee/configuration.rb
|
|
74
74
|
- lib/erbee/diagram_renderer.rb
|
|
75
75
|
- lib/erbee/model_info.rb
|
|
76
|
+
- lib/erbee/polymorphic_collector.rb
|
|
77
|
+
- lib/erbee/polymorphic_registry.rb
|
|
76
78
|
- lib/erbee/version.rb
|
|
77
79
|
- sig/erbee.rbs
|
|
78
80
|
homepage: https://github.com/allister0098/erbee
|