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 +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/rails_lens/erd/visualizer.rb +85 -219
- data/lib/rails_lens/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ea81d5fe631c0523042ac851e7eaf48de1342e7d6e830512df298b25dd799b09
|
4
|
+
data.tar.gz: f3e09fd1d9aa9b18c0df790356007dc9c952db84268945a284b9f4b496627372
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
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
|
-
|
89
|
-
|
54
|
+
attributes << {
|
55
|
+
type: type_str,
|
56
|
+
name: column.name,
|
57
|
+
keys: keys
|
58
|
+
}
|
90
59
|
end
|
91
60
|
|
92
|
-
#
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
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(
|
79
|
+
add_model_relationships(diagram, model, models)
|
126
80
|
end
|
127
81
|
|
128
|
-
# Generate mermaid syntax
|
129
|
-
mermaid_output =
|
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(
|
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(
|
147
|
+
add_belongs_to_relationship(diagram, model, association, target_model)
|
194
148
|
when :has_one
|
195
|
-
add_has_one_relationship(
|
149
|
+
add_has_one_relationship(diagram, model, association, target_model)
|
196
150
|
when :has_many
|
197
|
-
add_has_many_relationship(
|
151
|
+
add_has_many_relationship(diagram, model, association, target_model)
|
198
152
|
when :has_and_belongs_to_many
|
199
|
-
add_habtm_relationship(
|
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
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
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(
|
220
|
-
|
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(
|
228
|
-
|
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(
|
236
|
-
|
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)
|
data/lib/rails_lens/version.rb
CHANGED