rails_lens 0.2.6 → 0.2.8
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 +14 -0
- data/lib/rails_lens/erd/visualizer.rb +83 -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: 99f124c6bd77f455ffbfc0f09ee74d5c96f51d9ba4663926b32e775c3ba5eead
|
4
|
+
data.tar.gz: f31a75f58fd7716762f6be2e6b8003722499dc385235bdd52bac539499d2d5b9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7a69630b604fea32ccc5c07f05a1651e9fa4394c2ac6a2dbc616ee07640f4531137749c9aa16e3cb4480532c5884cd49e04ff63fbcaae7896df0b65f81041d66
|
7
|
+
data.tar.gz: 86105ae0fde1d28efb55cb7665133ab50040a873fa70a92e7e3a6950e8b562187109dd2c60d7c16368458a9ff35213971f53c5730ff3ce19ea92418c11d0ec3e
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [0.2.8](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.7...rails_lens/v0.2.8) (2025-08-14)
|
4
|
+
|
5
|
+
|
6
|
+
### Bug Fixes
|
7
|
+
|
8
|
+
* remove hard dependency of mermaid ([b82ea17](https://github.com/seuros/rails_lens/commit/b82ea17ccb0920321b5ab219bc63b3043d182ad3))
|
9
|
+
|
10
|
+
## [0.2.7](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.6...rails_lens/v0.2.7) (2025-08-14)
|
11
|
+
|
12
|
+
|
13
|
+
### Bug Fixes
|
14
|
+
|
15
|
+
* 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)
|
16
|
+
|
3
17
|
## [0.2.6](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.5...rails_lens/v0.2.6) (2025-08-06)
|
4
18
|
|
5
19
|
|
@@ -29,104 +29,56 @@ module RailsLens
|
|
29
29
|
return save_output(mermaid_output, 'mmd')
|
30
30
|
end
|
31
31
|
|
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?
|
32
|
+
# Create new ERDiagram using mermaid-ruby gem
|
33
|
+
diagram = Diagrams::ERDiagram.new
|
68
34
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
next unless has_data_source
|
74
|
-
|
75
|
-
model_display_name = format_model_name(model)
|
35
|
+
# Process models and add them to the diagram
|
36
|
+
models.each do |model|
|
37
|
+
# Skip abstract models
|
38
|
+
next if model.abstract_class?
|
76
39
|
|
77
|
-
|
78
|
-
|
79
|
-
|
40
|
+
# Skip models without valid tables/views or columns
|
41
|
+
is_view = ModelDetector.view_exists?(model)
|
42
|
+
has_data_source = is_view || (model.table_exists? && model.columns.present?)
|
43
|
+
next unless has_data_source
|
80
44
|
|
81
|
-
|
45
|
+
begin
|
46
|
+
# Create attributes for the entity
|
47
|
+
attributes = []
|
82
48
|
model.columns.each do |column|
|
83
49
|
type_str = format_column_type(column)
|
84
|
-
name_str = column.name
|
85
50
|
keys = determine_keys(model, column)
|
86
|
-
key_str = keys.map(&:to_s).join(' ')
|
87
51
|
|
88
|
-
|
89
|
-
|
52
|
+
attributes << {
|
53
|
+
type: type_str,
|
54
|
+
name: column.name,
|
55
|
+
keys: keys
|
56
|
+
}
|
90
57
|
end
|
91
58
|
|
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
|
59
|
+
# Add entity to diagram (model name will be automatically quoted if needed)
|
60
|
+
diagram.add_entity(
|
61
|
+
name: model.name,
|
62
|
+
attributes: attributes
|
63
|
+
)
|
64
|
+
|
65
|
+
RailsLens.logger.debug { "Added entity: #{model.name}" } if options[:verbose]
|
102
66
|
rescue StandardError => e
|
103
67
|
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
68
|
end
|
109
|
-
end
|
110
|
-
|
111
|
-
# Add visual styling for views vs tables
|
112
|
-
add_visual_styling(output, models)
|
113
69
|
|
114
|
-
|
115
|
-
output << ' %% Relationships'
|
116
|
-
models.each do |model|
|
117
|
-
# Skip abstract models in relationship generation too
|
70
|
+
# Add relationships
|
118
71
|
next if model.abstract_class?
|
119
72
|
|
120
|
-
# Include both table-backed and view-backed models
|
121
73
|
is_view = ModelDetector.view_exists?(model)
|
122
74
|
has_data_source = is_view || (model.table_exists? && model.columns.present?)
|
123
75
|
next unless has_data_source
|
124
76
|
|
125
|
-
add_model_relationships(
|
77
|
+
add_model_relationships(diagram, model, models)
|
126
78
|
end
|
127
79
|
|
128
|
-
# Generate mermaid syntax
|
129
|
-
mermaid_output =
|
80
|
+
# Generate mermaid syntax using the gem
|
81
|
+
mermaid_output = diagram.to_mermaid
|
130
82
|
|
131
83
|
# Save output
|
132
84
|
filename = save_output(mermaid_output, 'mmd')
|
@@ -159,7 +111,7 @@ module RailsLens
|
|
159
111
|
end
|
160
112
|
end
|
161
113
|
|
162
|
-
# Check unique indexes
|
114
|
+
# Check unique indexes - use UK which will be automatically quoted as comment
|
163
115
|
if model.connection.indexes(model.table_name).any? do |idx|
|
164
116
|
idx.unique && idx.columns.include?(column.name)
|
165
117
|
end && keys.exclude?(:PK)
|
@@ -169,7 +121,7 @@ module RailsLens
|
|
169
121
|
keys
|
170
122
|
end
|
171
123
|
|
172
|
-
def add_model_relationships(
|
124
|
+
def add_model_relationships(diagram, model, models)
|
173
125
|
model.reflect_on_all_associations.each do |association|
|
174
126
|
next if association.options[:through] # Skip through associations for now
|
175
127
|
next if association.polymorphic? # Skip polymorphic associations
|
@@ -190,177 +142,89 @@ module RailsLens
|
|
190
142
|
|
191
143
|
case association.macro
|
192
144
|
when :belongs_to
|
193
|
-
add_belongs_to_relationship(
|
145
|
+
add_belongs_to_relationship(diagram, model, association, target_model)
|
194
146
|
when :has_one
|
195
|
-
add_has_one_relationship(
|
147
|
+
add_has_one_relationship(diagram, model, association, target_model)
|
196
148
|
when :has_many
|
197
|
-
add_has_many_relationship(
|
149
|
+
add_has_many_relationship(diagram, model, association, target_model)
|
198
150
|
when :has_and_belongs_to_many
|
199
|
-
add_habtm_relationship(
|
151
|
+
add_habtm_relationship(diagram, model, association, target_model)
|
200
152
|
end
|
201
153
|
end
|
202
154
|
|
203
155
|
# Check for closure_tree self-reference - but only if model is not abstract
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
156
|
+
return unless model.respond_to?(:_ct) && !model.abstract_class?
|
157
|
+
|
158
|
+
diagram.add_relationship(
|
159
|
+
entity1: model.name,
|
160
|
+
entity2: model.name,
|
161
|
+
cardinality1: :ZERO_OR_MORE,
|
162
|
+
cardinality2: :ZERO_OR_MORE,
|
163
|
+
identifying: false,
|
164
|
+
label: 'closure_tree'
|
165
|
+
)
|
166
|
+
end
|
167
|
+
|
168
|
+
def add_belongs_to_relationship(diagram, model, association, target_model)
|
169
|
+
diagram.add_relationship(
|
170
|
+
entity1: model.name,
|
171
|
+
entity2: target_model.name,
|
172
|
+
cardinality1: :ZERO_OR_MORE,
|
173
|
+
cardinality2: :ONE_ONLY,
|
174
|
+
identifying: false,
|
175
|
+
label: association.name.to_s
|
176
|
+
)
|
213
177
|
rescue StandardError => e
|
214
178
|
RailsLens.logger.debug do
|
215
179
|
"Warning: Could not add belongs_to relationship #{model.name} -> #{association.name}: #{e.message}"
|
216
180
|
end
|
217
181
|
end
|
218
182
|
|
219
|
-
def add_has_one_relationship(
|
220
|
-
|
183
|
+
def add_has_one_relationship(diagram, model, association, target_model)
|
184
|
+
diagram.add_relationship(
|
185
|
+
entity1: model.name,
|
186
|
+
entity2: target_model.name,
|
187
|
+
cardinality1: :ONE_ONLY,
|
188
|
+
cardinality2: :ZERO_OR_ONE,
|
189
|
+
identifying: false,
|
190
|
+
label: association.name.to_s
|
191
|
+
)
|
221
192
|
rescue StandardError => e
|
222
193
|
RailsLens.logger.debug do
|
223
194
|
"Warning: Could not add has_one relationship #{model.name} -> #{association.name}: #{e.message}"
|
224
195
|
end
|
225
196
|
end
|
226
197
|
|
227
|
-
def add_has_many_relationship(
|
228
|
-
|
198
|
+
def add_has_many_relationship(diagram, model, association, target_model)
|
199
|
+
diagram.add_relationship(
|
200
|
+
entity1: model.name,
|
201
|
+
entity2: target_model.name,
|
202
|
+
cardinality1: :ONE_ONLY,
|
203
|
+
cardinality2: :ZERO_OR_MORE,
|
204
|
+
identifying: false,
|
205
|
+
label: association.name.to_s
|
206
|
+
)
|
229
207
|
rescue StandardError => e
|
230
208
|
RailsLens.logger.debug do
|
231
209
|
"Warning: Could not add has_many relationship #{model.name} -> #{association.name}: #{e.message}"
|
232
210
|
end
|
233
211
|
end
|
234
212
|
|
235
|
-
def add_habtm_relationship(
|
236
|
-
|
213
|
+
def add_habtm_relationship(diagram, model, association, target_model)
|
214
|
+
diagram.add_relationship(
|
215
|
+
entity1: model.name,
|
216
|
+
entity2: target_model.name,
|
217
|
+
cardinality1: :ZERO_OR_MORE,
|
218
|
+
cardinality2: :ZERO_OR_MORE,
|
219
|
+
identifying: false,
|
220
|
+
label: association.name.to_s
|
221
|
+
)
|
237
222
|
rescue StandardError => e
|
238
223
|
RailsLens.logger.debug do
|
239
224
|
"Warning: Could not add habtm relationship #{model.name} -> #{association.name}: #{e.message}"
|
240
225
|
end
|
241
226
|
end
|
242
227
|
|
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
228
|
def save_output(content, extension)
|
365
229
|
output_dir = config[:output_dir] || 'doc/erd'
|
366
230
|
FileUtils.mkdir_p(output_dir)
|
data/lib/rails_lens/version.rb
CHANGED