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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4058446709d36b729b7771aa2d261686e19586816fcac65c6de80d649c3df87f
4
- data.tar.gz: ba2528945f35dbb48d73ff594aea3d77f9f94d04cd55b85d738d66bbf789ea4e
3
+ metadata.gz: 30b52ebea6705191f221bc4b963b8c8d8a21220edd585baacb302f1d00065bee
4
+ data.tar.gz: 3311df2ced5a719ba38127c2db7f260048b495d7fda5ba406b7f7c07571f329c
5
5
  SHA512:
6
- metadata.gz: 79d37934763e32e268afb17ae0469238e968b50bd9616521d64929e3c3813de7abdcc113e338be108c6ba3794b1ee7e244013510ff5d10e9d733a8aa129f7cd9
7
- data.tar.gz: 18191084b2c14e416e5eeccb37b3880ec6699fe6dfa62d9736d4eb763bcc8cb3541b606c3a687a7fd4505dba55282cdc4c3f2b971b8bdddacbb3799c9dd2707c
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 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.
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
- ## Table of Contents
6
+ ---
6
7
 
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)
8
+ ## 1. Quick Installation & Setup
18
9
 
19
- ## Features
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
- - **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.
25
+ ## 2. Converting Mermaid .md to .svg
26
26
 
27
- ## Installation
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
- 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
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
- ### 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
38
+ Now you have er_diagram.svg in your project folder—open it in any browser or image viewer.
371
39
 
372
- # 4. Render the diagram
373
- renderer = Erbee::DiagramRenderer.new(model_infos)
374
- renderer.render
40
+ ## 3. View the SVG in Your Browser
375
41
 
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
42
+ Depending on your OS, run one of these commands:
43
+ - **macOS**:
44
+ ```bash
45
+ open er_diagram.svg
451
46
  ```
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"
47
+ - **Linux**:
48
+ ```bash
49
+ xdg-open er_diagram.svg
469
50
  ```
470
51
 
471
- When rendered with a Mermaid-compatible viewer, this will display an ER diagram showing a one-to-many relationship between `users` and `posts`.
52
+ After that, your browser (or default viewer) should display the diagram.
@@ -1,12 +1,15 @@
1
- require 'active_record'
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 = {} # { model_class => current_depth }
9
- @results = [] # Array of ModelInfo
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
- @results << model_info unless @results.any? { |m| m.model_class == model_class }
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
- model_class.reflect_on_all_associations.each do |assoc|
30
- next_model_class = assoc.klass
31
- traverse(next_model_class, current_depth + 1)
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.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
- }
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: model_class,
110
+ model_class: model_class,
54
111
  associations: associations,
55
- columns: 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 (TABLE1 ||--|{ TABLE2 : "1:N" )
30
+ # Relationships
31
31
  visited_edges = {}
32
32
  @model_infos.each do |model_info|
33
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
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
- dst_table = dst_info.model_class.table_name
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
- # 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
+ # 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
- # Get relation symbol
47
- relation_str = mermaid_relation(assoc[:type], src_table, dst_table)
48
- lines << " #{relation_str}"
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
- # Table definition section
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
- # e.g. "int id", "string name"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Erbee
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
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.0
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-01-19 00:00:00.000000000 Z
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