rails_lens 0.0.0 → 0.2.2

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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/LICENSE.txt +2 -2
  4. data/README.md +463 -9
  5. data/exe/rails_lens +25 -0
  6. data/lib/rails_lens/analyzers/association_analyzer.rb +111 -0
  7. data/lib/rails_lens/analyzers/base.rb +35 -0
  8. data/lib/rails_lens/analyzers/best_practices_analyzer.rb +114 -0
  9. data/lib/rails_lens/analyzers/column_analyzer.rb +97 -0
  10. data/lib/rails_lens/analyzers/composite_keys.rb +62 -0
  11. data/lib/rails_lens/analyzers/database_constraints.rb +35 -0
  12. data/lib/rails_lens/analyzers/delegated_types.rb +129 -0
  13. data/lib/rails_lens/analyzers/enums.rb +34 -0
  14. data/lib/rails_lens/analyzers/error_handling.rb +66 -0
  15. data/lib/rails_lens/analyzers/foreign_key_analyzer.rb +47 -0
  16. data/lib/rails_lens/analyzers/generated_columns.rb +56 -0
  17. data/lib/rails_lens/analyzers/index_analyzer.rb +128 -0
  18. data/lib/rails_lens/analyzers/inheritance.rb +212 -0
  19. data/lib/rails_lens/analyzers/notes.rb +325 -0
  20. data/lib/rails_lens/analyzers/performance_analyzer.rb +110 -0
  21. data/lib/rails_lens/annotation_pipeline.rb +87 -0
  22. data/lib/rails_lens/cli.rb +176 -0
  23. data/lib/rails_lens/cli_error_handler.rb +86 -0
  24. data/lib/rails_lens/commands.rb +164 -0
  25. data/lib/rails_lens/connection.rb +133 -0
  26. data/lib/rails_lens/erd/column_type_formatter.rb +32 -0
  27. data/lib/rails_lens/erd/domain_color_mapper.rb +40 -0
  28. data/lib/rails_lens/erd/mysql_column_type_formatter.rb +19 -0
  29. data/lib/rails_lens/erd/postgresql_column_type_formatter.rb +19 -0
  30. data/lib/rails_lens/erd/visualizer.rb +329 -0
  31. data/lib/rails_lens/errors.rb +78 -0
  32. data/lib/rails_lens/extension_loader.rb +261 -0
  33. data/lib/rails_lens/extensions/base.rb +194 -0
  34. data/lib/rails_lens/extensions/closure_tree_ext.rb +157 -0
  35. data/lib/rails_lens/file_insertion_helper.rb +168 -0
  36. data/lib/rails_lens/mailer/annotator.rb +226 -0
  37. data/lib/rails_lens/mailer/extractor.rb +201 -0
  38. data/lib/rails_lens/model_detector.rb +252 -0
  39. data/lib/rails_lens/parsers/class_info.rb +46 -0
  40. data/lib/rails_lens/parsers/module_info.rb +33 -0
  41. data/lib/rails_lens/parsers/parser_result.rb +55 -0
  42. data/lib/rails_lens/parsers/prism_parser.rb +90 -0
  43. data/lib/rails_lens/parsers.rb +10 -0
  44. data/lib/rails_lens/providers/association_notes_provider.rb +11 -0
  45. data/lib/rails_lens/providers/base.rb +37 -0
  46. data/lib/rails_lens/providers/best_practices_notes_provider.rb +11 -0
  47. data/lib/rails_lens/providers/column_notes_provider.rb +11 -0
  48. data/lib/rails_lens/providers/composite_keys_provider.rb +11 -0
  49. data/lib/rails_lens/providers/database_constraints_provider.rb +11 -0
  50. data/lib/rails_lens/providers/delegated_types_provider.rb +11 -0
  51. data/lib/rails_lens/providers/enums_provider.rb +11 -0
  52. data/lib/rails_lens/providers/extension_notes_provider.rb +20 -0
  53. data/lib/rails_lens/providers/extensions_provider.rb +22 -0
  54. data/lib/rails_lens/providers/foreign_key_notes_provider.rb +11 -0
  55. data/lib/rails_lens/providers/generated_columns_provider.rb +11 -0
  56. data/lib/rails_lens/providers/index_notes_provider.rb +20 -0
  57. data/lib/rails_lens/providers/inheritance_provider.rb +23 -0
  58. data/lib/rails_lens/providers/notes_provider_base.rb +25 -0
  59. data/lib/rails_lens/providers/performance_notes_provider.rb +11 -0
  60. data/lib/rails_lens/providers/schema_provider.rb +61 -0
  61. data/lib/rails_lens/providers/section_provider_base.rb +28 -0
  62. data/lib/rails_lens/railtie.rb +17 -0
  63. data/lib/rails_lens/rake_bootstrapper.rb +18 -0
  64. data/lib/rails_lens/route/annotator.rb +268 -0
  65. data/lib/rails_lens/route/extractor.rb +133 -0
  66. data/lib/rails_lens/route/parser.rb +59 -0
  67. data/lib/rails_lens/schema/adapters/base.rb +345 -0
  68. data/lib/rails_lens/schema/adapters/database_info.rb +118 -0
  69. data/lib/rails_lens/schema/adapters/mysql.rb +279 -0
  70. data/lib/rails_lens/schema/adapters/postgresql.rb +197 -0
  71. data/lib/rails_lens/schema/adapters/sqlite3.rb +96 -0
  72. data/lib/rails_lens/schema/annotation.rb +144 -0
  73. data/lib/rails_lens/schema/annotation_manager.rb +202 -0
  74. data/lib/rails_lens/tasks/annotate.rake +35 -0
  75. data/lib/rails_lens/tasks/erd.rake +24 -0
  76. data/lib/rails_lens/tasks/mailers.rake +27 -0
  77. data/lib/rails_lens/tasks/routes.rake +27 -0
  78. data/lib/rails_lens/tasks/schema.rake +108 -0
  79. data/lib/rails_lens/version.rb +5 -0
  80. data/lib/rails_lens.rb +138 -5
  81. metadata +215 -11
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ class Connection
5
+ class << self
6
+ def adapter_for(model_class)
7
+ connection = model_class.connection
8
+ adapter_name = detect_adapter_name(connection)
9
+
10
+ adapter_class = resolve_adapter_class(adapter_name)
11
+ adapter_class.new(connection, model_class.table_name)
12
+ end
13
+
14
+ def resolve_adapter_class(adapter_name)
15
+ class_name = "RailsLens::Schema::Adapters::#{adapter_name.to_s.camelize}"
16
+
17
+ begin
18
+ class_name.constantize
19
+ rescue NameError
20
+ raise RailsLens::UnsupportedAdapterError,
21
+ "Unsupported database adapter: #{adapter_name}. " \
22
+ "Expected adapter class #{class_name} not found. " \
23
+ 'Consider adding support by creating the adapter class.'
24
+ end
25
+ end
26
+
27
+ def detect_adapter_name(connection)
28
+ adapter_name = connection.adapter_name.downcase
29
+
30
+ case adapter_name
31
+ when /postgresql/, /postgis/
32
+ :postgresql
33
+ when /mysql2/, /trilogy/, /mariadb/
34
+ :mysql
35
+ when /sqlite/
36
+ :sqlite3
37
+ else
38
+ # Return the normalized adapter name for constantize
39
+ adapter_name.to_sym
40
+ end
41
+ end
42
+
43
+ def database_dialect(connection)
44
+ adapter_name = connection.adapter_name
45
+
46
+ case adapter_name.downcase
47
+ when /postgresql/, /postgis/
48
+ 'PostgreSQL'
49
+ when /mysql2/, /trilogy/
50
+ 'MySQL'
51
+ when /mariadb/
52
+ 'MariaDB'
53
+ when /sqlite/
54
+ 'SQLite'
55
+ else
56
+ adapter_name # Return the original adapter name if unknown
57
+ end
58
+ end
59
+
60
+ def connection_config(model_class)
61
+ if model_class.connection.respond_to?(:connection_db_config)
62
+ # Rails 6.1+
63
+ model_class.connection.connection_db_config.configuration_hash
64
+ elsif model_class.connection.respond_to?(:config)
65
+ # Older Rails versions
66
+ model_class.connection.config
67
+ else
68
+ {}
69
+ end
70
+ end
71
+
72
+ def database_version(model_class)
73
+ connection = model_class.connection
74
+
75
+ if connection.respond_to?(:database_version)
76
+ connection.database_version
77
+ elsif connection.respond_to?(:version)
78
+ connection.version
79
+ else
80
+ 'Unknown'
81
+ end
82
+ rescue StandardError
83
+ 'Unknown'
84
+ end
85
+
86
+ def connection_info(model_class)
87
+ {
88
+ adapter: detect_adapter_name(model_class.connection),
89
+ database: connection_config(model_class)[:database],
90
+ version: database_version(model_class),
91
+ encoding: connection_encoding(model_class)
92
+ }
93
+ end
94
+
95
+ def supports_foreign_keys?(model_class)
96
+ model_class.connection.supports_foreign_keys?
97
+ end
98
+
99
+ def supports_check_constraints?(model_class)
100
+ model_class.connection.supports_check_constraints?
101
+ end
102
+
103
+ def supports_comments?(model_class)
104
+ model_class.connection.supports_comments?
105
+ end
106
+
107
+ def supports_views?(model_class)
108
+ model_class.connection.supports_views?
109
+ end
110
+
111
+ def supports_materialized_views?(model_class)
112
+ model_class.connection.supports_materialized_views?
113
+ end
114
+
115
+ private
116
+
117
+ def connection_encoding(model_class)
118
+ connection = model_class.connection
119
+
120
+ if connection.respond_to?(:encoding)
121
+ connection.encoding
122
+ elsif connection.respond_to?(:charset)
123
+ connection.charset
124
+ else
125
+ config = connection_config(model_class)
126
+ config[:encoding] || config[:charset] || 'Unknown'
127
+ end
128
+ rescue StandardError
129
+ 'Unknown'
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ module ERD
5
+ class ColumnTypeFormatter
6
+ def self.format(column)
7
+ new(column).format
8
+ end
9
+
10
+ def initialize(column)
11
+ @column = column
12
+ end
13
+
14
+ def format
15
+ # Default to the generic type
16
+ case @column.type
17
+ when :integer, :bigint then 'int'
18
+ when :string, :text then 'varchar'
19
+ when :boolean then 'boolean'
20
+ when :decimal, :float then 'decimal'
21
+ when :date then 'date'
22
+ when :datetime, :timestamp then 'datetime'
23
+ when :time then 'time'
24
+ when :binary then 'blob'
25
+ when :json, :jsonb then 'json'
26
+ else
27
+ @column.type.to_s
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ module ERD
5
+ # Assigns a consistent color to a domain from a predefined palette.
6
+ class DomainColorMapper
7
+ # Default color palette for domains using CSS named colors.
8
+ DEFAULT_COLORS = %w[
9
+ lightblue
10
+ lightcoral
11
+ lightgreen
12
+ lightyellow
13
+ plum
14
+ lightcyan
15
+ lightgray
16
+ ].freeze
17
+
18
+ # The color used for domains not found in the initial list.
19
+ FALLBACK_COLOR = 'lightgray'
20
+
21
+ # @param domains [Array<Symbol>] A unique list of domain names.
22
+ # @param colors [Array<String>] A list of hex color codes to cycle through.
23
+ def initialize(domains, colors: DEFAULT_COLORS)
24
+ @colors = colors.empty? ? [FALLBACK_COLOR] : colors
25
+ @domain_map = domains.uniq.each_with_index.to_h
26
+ end
27
+
28
+ # Returns the color for a given domain.
29
+ #
30
+ # @param domain [Symbol] The domain name.
31
+ # @return [String] The hex color code.
32
+ def color_for(domain)
33
+ domain_index = @domain_map[domain]
34
+ return FALLBACK_COLOR unless domain_index
35
+
36
+ @colors[domain_index % @colors.length]
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ module ERD
5
+ class MysqlColumnTypeFormatter < ColumnTypeFormatter
6
+ def format
7
+ case @column.sql_type
8
+ when /json/i then 'json'
9
+ when /enum/i then 'enum'
10
+ when /set/i then 'set'
11
+ when /mediumtext/i then 'mediumtext'
12
+ when /tinyint\(1\)/i then 'boolean'
13
+ else
14
+ super
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ module ERD
5
+ class PostgresqlColumnTypeFormatter < ColumnTypeFormatter
6
+ def format
7
+ case @column.sql_type
8
+ when /jsonb/i then 'jsonb'
9
+ when /uuid/i then 'uuid'
10
+ when /inet/i then 'inet'
11
+ when /array/i then 'array'
12
+ when /tsvector/i then 'tsvector'
13
+ else
14
+ super
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,329 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ module ERD
5
+ class Visualizer
6
+ require_relative 'domain_color_mapper'
7
+ attr_reader :options, :config
8
+
9
+ def initialize(options: {})
10
+ @options = options
11
+ @config = RailsLens.config.erd.merge(options.compact.transform_keys(&:to_sym))
12
+ end
13
+
14
+ def generate
15
+ models = load_models
16
+ generate_mermaid(models)
17
+ end
18
+
19
+ private
20
+
21
+ def load_models
22
+ ModelDetector.detect_models(options)
23
+ end
24
+
25
+ def generate_mermaid(models)
26
+ if models.blank?
27
+ # Still need to save the output even if no models found
28
+ mermaid_output = "erDiagram\n %% No models found"
29
+ return save_output(mermaid_output, 'mmd')
30
+ end
31
+
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?
68
+
69
+ # Skip models without valid tables or columns
70
+ next unless model.table_exists? && model.columns.present?
71
+
72
+ model_display_name = format_model_name(model)
73
+
74
+ output << " #{model_display_name} {"
75
+ # Track opening brace position for error recovery
76
+ brace_position = output.size
77
+
78
+ columns_added = false
79
+ model.columns.each do |column|
80
+ type_str = format_column_type(column)
81
+ name_str = column.name
82
+ keys = determine_keys(model, column)
83
+ key_str = keys.map(&:to_s).join(' ')
84
+
85
+ output << " #{type_str} #{name_str}#{" #{key_str}" unless key_str.empty?}"
86
+ columns_added = true
87
+ end
88
+
89
+ # Only close the entity if we successfully added columns
90
+ if columns_added
91
+ output << ' }'
92
+ output << ''
93
+ Rails.logger.debug { "Added entity: #{model_display_name}" } if options[:verbose]
94
+ else
95
+ # Remove the opening brace if no columns were added
96
+ output.slice!(brace_position..-1)
97
+ Rails.logger.debug { "Skipped entity #{model_display_name}: no columns found" } if options[:verbose]
98
+ end
99
+ rescue StandardError => e
100
+ Rails.logger.debug { "Warning: Could not add entity #{model.name}: #{e.message}" }
101
+ # Remove any partial entity content added since the opening brace
102
+ if output.size > brace_position
103
+ output.slice!(brace_position..-1)
104
+ end
105
+ end
106
+ end
107
+
108
+ # Add relationships
109
+ output << ' %% Relationships'
110
+ models.each do |model|
111
+ # Skip abstract models in relationship generation too
112
+ next if model.abstract_class?
113
+ next unless model.table_exists? && model.columns.present?
114
+
115
+ add_model_relationships(output, model, models)
116
+ end
117
+
118
+ # Generate mermaid syntax
119
+ mermaid_output = output.join("\n")
120
+
121
+ # Save output
122
+ filename = save_output(mermaid_output, 'mmd')
123
+
124
+ Rails.logger.debug 'ERD generated successfully!'
125
+ filename # Return the filename instead of content
126
+ end
127
+
128
+ def format_column_type(column)
129
+ formatter_class = case column.sql_type
130
+ when /jsonb|uuid|inet|array|tsvector/i
131
+ PostgresqlColumnTypeFormatter
132
+ when /json|enum|set|mediumtext|tinyint\(1\)/i
133
+ MysqlColumnTypeFormatter
134
+ else
135
+ ColumnTypeFormatter
136
+ end
137
+
138
+ formatter_class.format(column)
139
+ end
140
+
141
+ def determine_keys(model, column)
142
+ keys = []
143
+ keys << :PK if column.name == model.primary_key
144
+
145
+ # Check foreign keys
146
+ if model.respond_to?(:reflect_on_all_associations)
147
+ model.reflect_on_all_associations(:belongs_to).each do |assoc|
148
+ keys << :FK if assoc.foreign_key.to_s == column.name
149
+ end
150
+ end
151
+
152
+ # Check unique indexes
153
+ if model.connection.indexes(model.table_name).any? do |idx|
154
+ idx.unique && idx.columns.include?(column.name)
155
+ end && keys.exclude?(:PK)
156
+ keys << :UK
157
+ end
158
+
159
+ keys
160
+ end
161
+
162
+ def add_model_relationships(output, model, models)
163
+ model.reflect_on_all_associations.each do |association|
164
+ next if association.options[:through] # Skip through associations for now
165
+ next if association.polymorphic? # Skip polymorphic associations
166
+
167
+ # Check if target model exists and has table
168
+ target_model = nil
169
+ begin
170
+ target_model = association.klass
171
+ rescue NameError, ArgumentError
172
+ next # Skip if class can't be loaded
173
+ end
174
+
175
+ next unless target_model && models.include?(target_model)
176
+
177
+ # Skip relationships to abstract models
178
+ next if target_model.abstract_class?
179
+ next unless target_model.table_exists? && target_model.columns.present?
180
+
181
+ case association.macro
182
+ when :belongs_to
183
+ add_belongs_to_relationship(output, model, association, target_model)
184
+ when :has_one
185
+ add_has_one_relationship(output, model, association, target_model)
186
+ when :has_many
187
+ add_has_many_relationship(output, model, association, target_model)
188
+ when :has_and_belongs_to_many
189
+ add_habtm_relationship(output, model, association, target_model)
190
+ end
191
+ end
192
+
193
+ # Check for closure_tree self-reference - but only if model is not abstract
194
+ # rubocop:disable Style/GuardClause
195
+ if model.respond_to?(:_ct) && !model.abstract_class?
196
+ output << " #{format_model_name(model)} }o--o{ #{format_model_name(model)} : \"closure_tree\""
197
+ end
198
+ # rubocop:enable Style/GuardClause
199
+ end
200
+
201
+ def add_belongs_to_relationship(output, model, association, target_model)
202
+ output << " #{format_model_name(model)} }o--|| #{format_model_name(target_model)} : \"#{association.name}\""
203
+ rescue StandardError => e
204
+ Rails.logger.debug do
205
+ "Warning: Could not add belongs_to relationship #{model.name} -> #{association.name}: #{e.message}"
206
+ end
207
+ end
208
+
209
+ def add_has_one_relationship(output, model, association, target_model)
210
+ output << " #{format_model_name(model)} ||--o| #{format_model_name(target_model)} : \"#{association.name}\""
211
+ rescue StandardError => e
212
+ Rails.logger.debug do
213
+ "Warning: Could not add has_one relationship #{model.name} -> #{association.name}: #{e.message}"
214
+ end
215
+ end
216
+
217
+ def add_has_many_relationship(output, model, association, target_model)
218
+ output << " #{format_model_name(model)} ||--o{ #{format_model_name(target_model)} : \"#{association.name}\""
219
+ rescue StandardError => e
220
+ Rails.logger.debug do
221
+ "Warning: Could not add has_many relationship #{model.name} -> #{association.name}: #{e.message}"
222
+ end
223
+ end
224
+
225
+ def add_habtm_relationship(output, model, association, target_model)
226
+ output << " #{format_model_name(model)} }o--o{ #{format_model_name(target_model)} : \"#{association.name}\""
227
+ rescue StandardError => e
228
+ Rails.logger.debug do
229
+ "Warning: Could not add habtm relationship #{model.name} -> #{association.name}: #{e.message}"
230
+ end
231
+ end
232
+
233
+ def add_theme_configuration(output)
234
+ # Get default color palette
235
+ default_colors = config[:default_colors] || DomainColorMapper::DEFAULT_COLORS
236
+
237
+ # Use first few colors for Mermaid theme
238
+ primary_color = default_colors[0] || 'lightgray'
239
+ secondary_color = default_colors[1] || 'lightblue'
240
+ tertiary_color = default_colors[2] || 'lightcoral'
241
+
242
+ # Mermaid theme directives
243
+ output << ' %%{init: {'
244
+ output << ' "theme": "default",'
245
+ output << ' "themeVariables": {'
246
+ output << " \"primaryColor\": \"#{primary_color}\","
247
+ output << ' "primaryTextColor": "#333",'
248
+ output << ' "primaryBorderColor": "#666",'
249
+ output << ' "lineColor": "#666",'
250
+ output << " \"secondaryColor\": \"#{secondary_color}\","
251
+ output << " \"tertiaryColor\": \"#{tertiary_color}\""
252
+ output << ' }'
253
+ output << ' }}%%'
254
+ end
255
+
256
+ def group_models_by_database(models)
257
+ grouped = Hash.new { |h, k| h[k] = [] }
258
+
259
+ models.each do |model|
260
+ # Get the database name from the model's connection
261
+ db_name = model.connection.pool.db_config.name
262
+ grouped[db_name] << model
263
+ rescue StandardError => e
264
+ Rails.logger.debug { "Warning: Could not determine database for #{model.name}: #{e.message}" }
265
+ grouped['unknown'] << model
266
+ end
267
+
268
+ # Sort databases for consistent output
269
+ grouped.sort_by { |db_name, _| db_name.to_s }.to_h
270
+ end
271
+
272
+ def group_models_by_domain(models)
273
+ grouped = Hash.new { |h, k| h[k] = [] }
274
+
275
+ models.each do |model|
276
+ domain = determine_model_domain(model)
277
+ grouped[domain] << model
278
+ end
279
+
280
+ # Sort domains for consistent output
281
+ grouped.sort_by { |domain, _| domain.to_s }.to_h
282
+ end
283
+
284
+ def determine_model_domain(model)
285
+ model_name = model.name.downcase
286
+
287
+ # Basic domain detection based on common patterns
288
+ return :auth if model_name.match?(/user|account|session|authentication|authorization/)
289
+ return :content if model_name.match?(/post|article|comment|blog|page|content/)
290
+ return :commerce if model_name.match?(/product|order|payment|cart|invoice|transaction/)
291
+ return :core if model_name.match?(/category|tag|setting|configuration|notification/)
292
+
293
+ # Default domain
294
+ :general
295
+ end
296
+
297
+ def create_domain_color_mapper(domains)
298
+ # Get colors from config or use defaults
299
+ colors = config[:default_colors] || DomainColorMapper::DEFAULT_COLORS
300
+ DomainColorMapper.new(domains, colors: colors)
301
+ end
302
+
303
+ def format_model_name(model)
304
+ return model.name unless config[:include_all_databases] || config[:show_database_labels]
305
+
306
+ # Get database name from the model's connection
307
+ begin
308
+ db_name = model.connection.pool.db_config.name
309
+ return model.name if db_name == 'primary' # Don't prefix primary database models
310
+
311
+ "#{model.name}[#{db_name}]"
312
+ rescue StandardError
313
+ model.name
314
+ end
315
+ end
316
+
317
+ def save_output(content, extension)
318
+ output_dir = config[:output_dir] || 'doc/erd'
319
+ FileUtils.mkdir_p(output_dir)
320
+
321
+ filename = File.join(output_dir, "erd.#{extension}")
322
+ File.write(filename, content)
323
+
324
+ Rails.logger.debug { "ERD saved to: #{filename}" }
325
+ filename # Return the filename
326
+ end
327
+ end
328
+ end
329
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ # Base error class for all Rails Lens errors
5
+ class Error < StandardError; end
6
+
7
+ # Configuration-related errors
8
+ class ConfigurationError < Error; end
9
+
10
+ # Model detection errors
11
+ class ModelDetectionError < Error; end
12
+ class ModelNotFoundError < ModelDetectionError; end
13
+ class InvalidModelError < ModelDetectionError; end
14
+
15
+ # Database-related errors
16
+ class DatabaseError < Error; end
17
+ class ConnectionError < DatabaseError; end
18
+ class SchemaError < DatabaseError; end
19
+ class TableNotFoundError < DatabaseError; end
20
+ class UnsupportedAdapterError < DatabaseError; end
21
+
22
+ # Annotation-related errors
23
+ class AnnotationError < Error; end
24
+ class FileNotFoundError < AnnotationError; end
25
+ class ParseError < AnnotationError; end
26
+ class InsertionError < AnnotationError; end
27
+
28
+ # Extension-related errors
29
+ class ExtensionError < Error; end
30
+ class ExtensionLoadError < ExtensionError; end
31
+ class ExtensionConfigError < ExtensionError; end
32
+
33
+ # Analysis errors
34
+ class AnalysisError < Error; end
35
+ class AnalyzerError < AnalysisError; end
36
+
37
+ # ERD generation errors
38
+ class ERDError < Error; end
39
+ class VisualizationError < ERDError; end
40
+
41
+ # Error reporter for centralized error handling
42
+ class ErrorReporter
43
+ class << self
44
+ def report(error, context = {})
45
+ return unless RailsLens.verbose || RailsLens.debug
46
+
47
+ message = build_error_message(error, context)
48
+
49
+ # Use Rails logger for verbose mode
50
+ Rails.logger&.error message
51
+
52
+ # Use kernel output for debug mode to ensure visibility
53
+ return unless RailsLens.debug
54
+
55
+ Rails.logger.debug message
56
+ end
57
+
58
+ def handle(context = {})
59
+ yield
60
+ rescue StandardError => e
61
+ report(e, context)
62
+ raise if RailsLens.raise_on_error
63
+
64
+ nil
65
+ end
66
+
67
+ private
68
+
69
+ def build_error_message(error, context)
70
+ message = ['[RailsLens Error]']
71
+ message << "Context: #{context.inspect}" if context.any?
72
+ message << "#{error.class}: #{error.message}"
73
+ message << error.backtrace.first(5).join("\n") if error.backtrace && RailsLens.config.debug
74
+ message.join("\n")
75
+ end
76
+ end
77
+ end
78
+ end