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 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
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+
4
+ Style/StringLiterals:
5
+ EnforcedStyle: double_quotes
6
+
7
+ Style/StringLiteralsInInterpolation:
8
+ EnforcedStyle: double_quotes
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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/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,11 @@
1
+ module Erbee
2
+ class Configuration
3
+ attr_accessor :depth, :output_path
4
+
5
+ def initialize
6
+ @depth = 2
7
+ @output_path = "er_diagram.md"
8
+ end
9
+ end
10
+ end
11
+
@@ -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
@@ -0,0 +1,9 @@
1
+ module Erbee
2
+ ModelInfo = Struct.new(
3
+ :model_class,
4
+ :associations,
5
+ :columns,
6
+ keyword_init: true
7
+ )
8
+ end
9
+
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Erbee
4
+ VERSION = "0.1.0"
5
+ 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
@@ -0,0 +1,4 @@
1
+ module Erbee
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []