rails_lens 0.2.6 → 0.2.7

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: d9c79f3cf64c15c090934062200871d4166ecc1a68ae4942a078ea4bf39c7c09
4
- data.tar.gz: b91275cfeb9c312c6498da217eb0b907e8e6e220793007a75ea27b5d9b6f19e3
3
+ metadata.gz: ea81d5fe631c0523042ac851e7eaf48de1342e7d6e830512df298b25dd799b09
4
+ data.tar.gz: f3e09fd1d9aa9b18c0df790356007dc9c952db84268945a284b9f4b496627372
5
5
  SHA512:
6
- metadata.gz: 0ccba97c7621956220bce1cfb2c2c6f1152fec52bf95b70247a53e20ccacea4ad414575d41a27a41b43b6818ad9e1d617a57e594820c8d789f1d917e78018f15
7
- data.tar.gz: 803764fabe90e5029a46682063980601dba2c8775db3b7e0eb095751270dfce77028ccbe0940876c058b9cc6f0c0d1d909885af7c16a86457a1757af143a0739
6
+ metadata.gz: cfaa977bdbbdb4496a46bbe0fcfa652814710ca25af99e0bbd9f4e71aa848a55d4085b9ebcf6e4813188ca98d36211add950feb2d3deee724d2bee5d19d1706d
7
+ data.tar.gz: e74ab70f37dc32b8584800eeb2cd71acf01b0b17680777034bb188d966e1d1c797a91f3edd92131ca291fa6fd4dfb1b47bc1374cf020d0e8c93e9f7ac280477e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.7](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.6...rails_lens/v0.2.7) (2025-08-14)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * use mermaid gem as backend ([#19](https://github.com/seuros/rails_lens/issues/19)) ([2297ecb](https://github.com/seuros/rails_lens/commit/2297ecb1a61ae1c3bb3ea4b1f602f9bea91a5aa8)), closes [#18](https://github.com/seuros/rails_lens/issues/18)
9
+
3
10
  ## [0.2.6](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.5...rails_lens/v0.2.6) (2025-08-06)
4
11
 
5
12
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'mermaid'
4
+
3
5
  module RailsLens
4
6
  module ERD
5
7
  class Visualizer
@@ -29,104 +31,56 @@ module RailsLens
29
31
  return save_output(mermaid_output, 'mmd')
30
32
  end
31
33
 
32
- output = ['erDiagram']
33
-
34
- # Add theme configuration
35
- if config[:theme] || config[:colors]
36
- output << ''
37
- output << ' %% Theme Configuration'
38
- add_theme_configuration(output)
39
- output << ''
40
- end
41
-
42
- # Choose grouping strategy based on configuration
43
- grouped_models = if config[:group_by_database]
44
- # Group models by database connection
45
- group_models_by_database(models)
46
- else
47
- # Group models by domain (existing behavior)
48
- group_models_by_domain(models)
49
- end
50
-
51
- # Create color mapper for domains (for future extensibility)
52
- unless config[:group_by_database]
53
- domain_list = grouped_models.keys.sort
54
- @color_mapper = create_domain_color_mapper(domain_list)
55
- end
56
-
57
- # Add entities
58
- grouped_models.each do |group_key, group_models|
59
- if config[:group_by_database]
60
- output << " %% Database: #{group_key}"
61
- elsif group_key != :general
62
- output << " %% #{group_key.to_s.humanize} Domain"
63
- end
64
-
65
- group_models.each do |model|
66
- # Additional safety check: Skip abstract models that might have slipped through
67
- next if model.abstract_class?
34
+ # Create new ERDiagram using mermaid-ruby gem
35
+ diagram = Diagrams::ERDiagram.new
68
36
 
69
- # Skip models without valid tables/views or columns
70
- # Include both table-backed and view-backed models
71
- is_view = ModelDetector.view_exists?(model)
72
- has_data_source = is_view || (model.table_exists? && model.columns.present?)
73
- next unless has_data_source
74
-
75
- model_display_name = format_model_name(model)
37
+ # Process models and add them to the diagram
38
+ models.each do |model|
39
+ # Skip abstract models
40
+ next if model.abstract_class?
76
41
 
77
- output << " #{model_display_name} {"
78
- # Track opening brace position for error recovery
79
- brace_position = output.size
42
+ # Skip models without valid tables/views or columns
43
+ is_view = ModelDetector.view_exists?(model)
44
+ has_data_source = is_view || (model.table_exists? && model.columns.present?)
45
+ next unless has_data_source
80
46
 
81
- columns_added = false
47
+ begin
48
+ # Create attributes for the entity
49
+ attributes = []
82
50
  model.columns.each do |column|
83
51
  type_str = format_column_type(column)
84
- name_str = column.name
85
52
  keys = determine_keys(model, column)
86
- key_str = keys.map(&:to_s).join(' ')
87
53
 
88
- output << " #{type_str} #{name_str}#{" #{key_str}" unless key_str.empty?}"
89
- columns_added = true
54
+ attributes << {
55
+ type: type_str,
56
+ name: column.name,
57
+ keys: keys
58
+ }
90
59
  end
91
60
 
92
- # Only close the entity if we successfully added columns
93
- if columns_added
94
- output << ' }'
95
- output << ''
96
- RailsLens.logger.debug { "Added entity: #{model_display_name}" } if options[:verbose]
97
- else
98
- # Remove the opening brace if no columns were added
99
- output.slice!(brace_position..-1)
100
- RailsLens.logger.debug { "Skipped entity #{model_display_name}: no columns found" } if options[:verbose]
101
- end
61
+ # Add entity to diagram (model name will be automatically quoted if needed)
62
+ diagram.add_entity(
63
+ name: model.name,
64
+ attributes: attributes
65
+ )
66
+
67
+ RailsLens.logger.debug { "Added entity: #{model.name}" } if options[:verbose]
102
68
  rescue StandardError => e
103
69
  RailsLens.logger.debug { "Warning: Could not add entity #{model.name}: #{e.message}" }
104
- # Remove any partial entity content added since the opening brace
105
- if output.size > brace_position
106
- output.slice!(brace_position..-1)
107
- end
108
70
  end
109
- end
110
-
111
- # Add visual styling for views vs tables
112
- add_visual_styling(output, models)
113
71
 
114
- # Add relationships
115
- output << ' %% Relationships'
116
- models.each do |model|
117
- # Skip abstract models in relationship generation too
72
+ # Add relationships
118
73
  next if model.abstract_class?
119
74
 
120
- # Include both table-backed and view-backed models
121
75
  is_view = ModelDetector.view_exists?(model)
122
76
  has_data_source = is_view || (model.table_exists? && model.columns.present?)
123
77
  next unless has_data_source
124
78
 
125
- add_model_relationships(output, model, models)
79
+ add_model_relationships(diagram, model, models)
126
80
  end
127
81
 
128
- # Generate mermaid syntax
129
- mermaid_output = output.join("\n")
82
+ # Generate mermaid syntax using the gem
83
+ mermaid_output = diagram.to_mermaid
130
84
 
131
85
  # Save output
132
86
  filename = save_output(mermaid_output, 'mmd')
@@ -159,7 +113,7 @@ module RailsLens
159
113
  end
160
114
  end
161
115
 
162
- # Check unique indexes
116
+ # Check unique indexes - use UK which will be automatically quoted as comment
163
117
  if model.connection.indexes(model.table_name).any? do |idx|
164
118
  idx.unique && idx.columns.include?(column.name)
165
119
  end && keys.exclude?(:PK)
@@ -169,7 +123,7 @@ module RailsLens
169
123
  keys
170
124
  end
171
125
 
172
- def add_model_relationships(output, model, models)
126
+ def add_model_relationships(diagram, model, models)
173
127
  model.reflect_on_all_associations.each do |association|
174
128
  next if association.options[:through] # Skip through associations for now
175
129
  next if association.polymorphic? # Skip polymorphic associations
@@ -190,177 +144,89 @@ module RailsLens
190
144
 
191
145
  case association.macro
192
146
  when :belongs_to
193
- add_belongs_to_relationship(output, model, association, target_model)
147
+ add_belongs_to_relationship(diagram, model, association, target_model)
194
148
  when :has_one
195
- add_has_one_relationship(output, model, association, target_model)
149
+ add_has_one_relationship(diagram, model, association, target_model)
196
150
  when :has_many
197
- add_has_many_relationship(output, model, association, target_model)
151
+ add_has_many_relationship(diagram, model, association, target_model)
198
152
  when :has_and_belongs_to_many
199
- add_habtm_relationship(output, model, association, target_model)
153
+ add_habtm_relationship(diagram, model, association, target_model)
200
154
  end
201
155
  end
202
156
 
203
157
  # Check for closure_tree self-reference - but only if model is not abstract
204
- # rubocop:disable Style/GuardClause
205
- if model.respond_to?(:_ct) && !model.abstract_class?
206
- output << " #{format_model_name(model)} }o--o{ #{format_model_name(model)} : \"closure_tree\""
207
- end
208
- # rubocop:enable Style/GuardClause
209
- end
210
-
211
- def add_belongs_to_relationship(output, model, association, target_model)
212
- output << " #{format_model_name(model)} }o--|| #{format_model_name(target_model)} : \"#{association.name}\""
158
+ return unless model.respond_to?(:_ct) && !model.abstract_class?
159
+
160
+ diagram.add_relationship(
161
+ entity1: model.name,
162
+ entity2: model.name,
163
+ cardinality1: :ZERO_OR_MORE,
164
+ cardinality2: :ZERO_OR_MORE,
165
+ identifying: false,
166
+ label: 'closure_tree'
167
+ )
168
+ end
169
+
170
+ def add_belongs_to_relationship(diagram, model, association, target_model)
171
+ diagram.add_relationship(
172
+ entity1: model.name,
173
+ entity2: target_model.name,
174
+ cardinality1: :ZERO_OR_MORE,
175
+ cardinality2: :ONE_ONLY,
176
+ identifying: false,
177
+ label: association.name.to_s
178
+ )
213
179
  rescue StandardError => e
214
180
  RailsLens.logger.debug do
215
181
  "Warning: Could not add belongs_to relationship #{model.name} -> #{association.name}: #{e.message}"
216
182
  end
217
183
  end
218
184
 
219
- def add_has_one_relationship(output, model, association, target_model)
220
- output << " #{format_model_name(model)} ||--o| #{format_model_name(target_model)} : \"#{association.name}\""
185
+ def add_has_one_relationship(diagram, model, association, target_model)
186
+ diagram.add_relationship(
187
+ entity1: model.name,
188
+ entity2: target_model.name,
189
+ cardinality1: :ONE_ONLY,
190
+ cardinality2: :ZERO_OR_ONE,
191
+ identifying: false,
192
+ label: association.name.to_s
193
+ )
221
194
  rescue StandardError => e
222
195
  RailsLens.logger.debug do
223
196
  "Warning: Could not add has_one relationship #{model.name} -> #{association.name}: #{e.message}"
224
197
  end
225
198
  end
226
199
 
227
- def add_has_many_relationship(output, model, association, target_model)
228
- output << " #{format_model_name(model)} ||--o{ #{format_model_name(target_model)} : \"#{association.name}\""
200
+ def add_has_many_relationship(diagram, model, association, target_model)
201
+ diagram.add_relationship(
202
+ entity1: model.name,
203
+ entity2: target_model.name,
204
+ cardinality1: :ONE_ONLY,
205
+ cardinality2: :ZERO_OR_MORE,
206
+ identifying: false,
207
+ label: association.name.to_s
208
+ )
229
209
  rescue StandardError => e
230
210
  RailsLens.logger.debug do
231
211
  "Warning: Could not add has_many relationship #{model.name} -> #{association.name}: #{e.message}"
232
212
  end
233
213
  end
234
214
 
235
- def add_habtm_relationship(output, model, association, target_model)
236
- output << " #{format_model_name(model)} }o--o{ #{format_model_name(target_model)} : \"#{association.name}\""
215
+ def add_habtm_relationship(diagram, model, association, target_model)
216
+ diagram.add_relationship(
217
+ entity1: model.name,
218
+ entity2: target_model.name,
219
+ cardinality1: :ZERO_OR_MORE,
220
+ cardinality2: :ZERO_OR_MORE,
221
+ identifying: false,
222
+ label: association.name.to_s
223
+ )
237
224
  rescue StandardError => e
238
225
  RailsLens.logger.debug do
239
226
  "Warning: Could not add habtm relationship #{model.name} -> #{association.name}: #{e.message}"
240
227
  end
241
228
  end
242
229
 
243
- def add_theme_configuration(output)
244
- # Get default color palette
245
- default_colors = config[:default_colors] || DomainColorMapper::DEFAULT_COLORS
246
-
247
- # Use first few colors for Mermaid theme
248
- primary_color = default_colors[0] || 'lightgray'
249
- secondary_color = default_colors[1] || 'lightblue'
250
- tertiary_color = default_colors[2] || 'lightcoral'
251
-
252
- # Mermaid theme directives
253
- output << ' %%{init: {'
254
- output << ' "theme": "default",'
255
- output << ' "themeVariables": {'
256
- output << " \"primaryColor\": \"#{primary_color}\","
257
- output << ' "primaryTextColor": "#333",'
258
- output << ' "primaryBorderColor": "#666",'
259
- output << ' "lineColor": "#666",'
260
- output << " \"secondaryColor\": \"#{secondary_color}\","
261
- output << " \"tertiaryColor\": \"#{tertiary_color}\""
262
- output << ' }'
263
- output << ' }}%%'
264
- end
265
-
266
- def add_visual_styling(output, models)
267
- # Add class definitions for visual distinction between tables and views
268
- output << ''
269
- output << ' %% Entity Styling'
270
-
271
- # Define styling classes
272
- output << ' classDef tableEntity fill:#f9f9f9,stroke:#333,stroke-width:2px'
273
- output << ' classDef viewEntity fill:#e6f3ff,stroke:#333,stroke-width:2px,stroke-dasharray: 5 5'
274
- output << ' classDef materializedViewEntity fill:#ffe6e6,stroke:#333,stroke-width:3px,stroke-dasharray: 5 5'
275
-
276
- # Apply styling to each model
277
- models.each do |model|
278
- next if model.abstract_class?
279
-
280
- is_view = ModelDetector.view_exists?(model)
281
- has_data_source = is_view || (model.table_exists? && model.columns.present?)
282
- next unless has_data_source
283
-
284
- model_display_name = format_model_name(model)
285
-
286
- if is_view
287
- view_metadata = ViewMetadata.new(model)
288
- output << if view_metadata.materialized_view?
289
- " class #{model_display_name} materializedViewEntity"
290
- else
291
- " class #{model_display_name} viewEntity"
292
- end
293
- else
294
- output << " class #{model_display_name} tableEntity"
295
- end
296
- rescue StandardError => e
297
- RailsLens.logger.debug { "Warning: Could not apply styling to #{model.name}: #{e.message}" }
298
- end
299
-
300
- output << ''
301
- end
302
-
303
- def group_models_by_database(models)
304
- grouped = Hash.new { |h, k| h[k] = [] }
305
-
306
- models.each do |model|
307
- # Get the database name from the model's connection
308
- db_name = model.connection.pool.db_config.name
309
- grouped[db_name] << model
310
- rescue StandardError => e
311
- RailsLens.logger.debug { "Warning: Could not determine database for #{model.name}: #{e.message}" }
312
- grouped['unknown'] << model
313
- end
314
-
315
- # Sort databases for consistent output
316
- grouped.sort_by { |db_name, _| db_name.to_s }.to_h
317
- end
318
-
319
- def group_models_by_domain(models)
320
- grouped = Hash.new { |h, k| h[k] = [] }
321
-
322
- models.each do |model|
323
- domain = determine_model_domain(model)
324
- grouped[domain] << model
325
- end
326
-
327
- # Sort domains for consistent output
328
- grouped.sort_by { |domain, _| domain.to_s }.to_h
329
- end
330
-
331
- def determine_model_domain(model)
332
- model_name = model.name.downcase
333
-
334
- # Basic domain detection based on common patterns
335
- return :auth if model_name.match?(/user|account|session|authentication|authorization/)
336
- return :content if model_name.match?(/post|article|comment|blog|page|content/)
337
- return :commerce if model_name.match?(/product|order|payment|cart|invoice|transaction/)
338
- return :core if model_name.match?(/category|tag|setting|configuration|notification/)
339
-
340
- # Default domain
341
- :general
342
- end
343
-
344
- def create_domain_color_mapper(domains)
345
- # Get colors from config or use defaults
346
- colors = config[:default_colors] || DomainColorMapper::DEFAULT_COLORS
347
- DomainColorMapper.new(domains, colors: colors)
348
- end
349
-
350
- def format_model_name(model)
351
- return model.name unless config[:include_all_databases] || config[:show_database_labels]
352
-
353
- # Get database name from the model's connection
354
- begin
355
- db_name = model.connection.pool.db_config.name
356
- return model.name if db_name == 'primary' # Don't prefix primary database models
357
-
358
- "#{model.name}[#{db_name}]"
359
- rescue StandardError
360
- model.name
361
- end
362
- end
363
-
364
230
  def save_output(content, extension)
365
231
  output_dir = config[:output_dir] || 'doc/erd'
366
232
  FileUtils.mkdir_p(output_dir)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsLens
4
- VERSION = '0.2.6'
4
+ VERSION = '0.2.7'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_lens
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih