rails_lens 0.0.0 → 0.2.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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/LICENSE.txt +2 -2
  4. data/README.md +393 -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 +124 -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 +170 -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 +301 -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 +241 -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 +48 -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,301 @@
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
+ model_display_name = format_model_name(model)
67
+ output << " #{model_display_name} {"
68
+
69
+ model.columns.each do |column|
70
+ type_str = format_column_type(column)
71
+ name_str = column.name
72
+ keys = determine_keys(model, column)
73
+ key_str = keys.map(&:to_s).join(' ')
74
+
75
+ output << " #{type_str} #{name_str}#{" #{key_str}" unless key_str.empty?}"
76
+ end
77
+
78
+ output << ' }'
79
+ output << ''
80
+
81
+ Rails.logger.debug { "Added entity: #{model_display_name}" } if options[:verbose]
82
+ rescue StandardError => e
83
+ Rails.logger.debug { "Warning: Could not add entity #{model.name}: #{e.message}" }
84
+ # Don't add partial entity if there's an error
85
+ # Remove the opening brace line if it was added
86
+ output.pop if output.last&.end_with?(' {')
87
+ end
88
+ end
89
+
90
+ # Add relationships
91
+ output << ' %% Relationships'
92
+ models.each do |model|
93
+ add_model_relationships(output, model, models)
94
+ end
95
+
96
+ # Generate mermaid syntax
97
+ mermaid_output = output.join("\n")
98
+
99
+ # Save output
100
+ filename = save_output(mermaid_output, 'mmd')
101
+
102
+ Rails.logger.debug 'ERD generated successfully!'
103
+ filename # Return the filename instead of content
104
+ end
105
+
106
+ def format_column_type(column)
107
+ formatter_class = case column.sql_type
108
+ when /jsonb|uuid|inet|array|tsvector/i
109
+ PostgresqlColumnTypeFormatter
110
+ when /json|enum|set|mediumtext|tinyint\(1\)/i
111
+ MysqlColumnTypeFormatter
112
+ else
113
+ ColumnTypeFormatter
114
+ end
115
+
116
+ formatter_class.format(column)
117
+ end
118
+
119
+ def determine_keys(model, column)
120
+ keys = []
121
+ keys << :PK if column.name == model.primary_key
122
+
123
+ # Check foreign keys
124
+ if model.respond_to?(:reflect_on_all_associations)
125
+ model.reflect_on_all_associations(:belongs_to).each do |assoc|
126
+ keys << :FK if assoc.foreign_key.to_s == column.name
127
+ end
128
+ end
129
+
130
+ # Check unique indexes
131
+ if model.connection.indexes(model.table_name).any? do |idx|
132
+ idx.unique && idx.columns.include?(column.name)
133
+ end && keys.exclude?(:PK)
134
+ keys << :UK
135
+ end
136
+
137
+ keys
138
+ end
139
+
140
+ def add_model_relationships(output, model, models)
141
+ model.reflect_on_all_associations.each do |association|
142
+ next if association.options[:through] # Skip through associations for now
143
+ next if association.polymorphic? # Skip polymorphic associations
144
+
145
+ # Check if target model exists and has table
146
+ target_model = nil
147
+ begin
148
+ target_model = association.klass
149
+ rescue NameError, ArgumentError
150
+ next # Skip if class can't be loaded
151
+ end
152
+
153
+ next unless target_model && models.include?(target_model)
154
+
155
+ case association.macro
156
+ when :belongs_to
157
+ add_belongs_to_relationship(output, model, association, target_model)
158
+ when :has_one
159
+ add_has_one_relationship(output, model, association, target_model)
160
+ when :has_many
161
+ add_has_many_relationship(output, model, association, target_model)
162
+ when :has_and_belongs_to_many
163
+ add_habtm_relationship(output, model, association, target_model)
164
+ end
165
+ end
166
+
167
+ # Check for closure_tree self-reference
168
+ return unless model.respond_to?(:_ct)
169
+
170
+ output << " #{format_model_name(model)} }o--o{ #{format_model_name(model)} : \"closure_tree\""
171
+ end
172
+
173
+ def add_belongs_to_relationship(output, model, association, target_model)
174
+ output << " #{format_model_name(model)} }o--|| #{format_model_name(target_model)} : \"#{association.name}\""
175
+ rescue StandardError => e
176
+ Rails.logger.debug do
177
+ "Warning: Could not add belongs_to relationship #{model.name} -> #{association.name}: #{e.message}"
178
+ end
179
+ end
180
+
181
+ def add_has_one_relationship(output, model, association, target_model)
182
+ output << " #{format_model_name(model)} ||--o| #{format_model_name(target_model)} : \"#{association.name}\""
183
+ rescue StandardError => e
184
+ Rails.logger.debug do
185
+ "Warning: Could not add has_one relationship #{model.name} -> #{association.name}: #{e.message}"
186
+ end
187
+ end
188
+
189
+ def add_has_many_relationship(output, model, association, target_model)
190
+ output << " #{format_model_name(model)} ||--o{ #{format_model_name(target_model)} : \"#{association.name}\""
191
+ rescue StandardError => e
192
+ Rails.logger.debug do
193
+ "Warning: Could not add has_many relationship #{model.name} -> #{association.name}: #{e.message}"
194
+ end
195
+ end
196
+
197
+ def add_habtm_relationship(output, model, association, target_model)
198
+ output << " #{format_model_name(model)} }o--o{ #{format_model_name(target_model)} : \"#{association.name}\""
199
+ rescue StandardError => e
200
+ Rails.logger.debug do
201
+ "Warning: Could not add habtm relationship #{model.name} -> #{association.name}: #{e.message}"
202
+ end
203
+ end
204
+
205
+ def add_theme_configuration(output)
206
+ # Get default color palette
207
+ default_colors = config[:default_colors] || DomainColorMapper::DEFAULT_COLORS
208
+
209
+ # Use first few colors for Mermaid theme
210
+ primary_color = default_colors[0] || 'lightgray'
211
+ secondary_color = default_colors[1] || 'lightblue'
212
+ tertiary_color = default_colors[2] || 'lightcoral'
213
+
214
+ # Mermaid theme directives
215
+ output << ' %%{init: {'
216
+ output << ' "theme": "default",'
217
+ output << ' "themeVariables": {'
218
+ output << " \"primaryColor\": \"#{primary_color}\","
219
+ output << ' "primaryTextColor": "#333",'
220
+ output << ' "primaryBorderColor": "#666",'
221
+ output << ' "lineColor": "#666",'
222
+ output << " \"secondaryColor\": \"#{secondary_color}\","
223
+ output << " \"tertiaryColor\": \"#{tertiary_color}\""
224
+ output << ' }'
225
+ output << ' }}%%'
226
+ end
227
+
228
+ def group_models_by_database(models)
229
+ grouped = Hash.new { |h, k| h[k] = [] }
230
+
231
+ models.each do |model|
232
+ # Get the database name from the model's connection
233
+ db_name = model.connection.pool.db_config.name
234
+ grouped[db_name] << model
235
+ rescue StandardError => e
236
+ Rails.logger.debug { "Warning: Could not determine database for #{model.name}: #{e.message}" }
237
+ grouped['unknown'] << model
238
+ end
239
+
240
+ # Sort databases for consistent output
241
+ grouped.sort_by { |db_name, _| db_name.to_s }.to_h
242
+ end
243
+
244
+ def group_models_by_domain(models)
245
+ grouped = Hash.new { |h, k| h[k] = [] }
246
+
247
+ models.each do |model|
248
+ domain = determine_model_domain(model)
249
+ grouped[domain] << model
250
+ end
251
+
252
+ # Sort domains for consistent output
253
+ grouped.sort_by { |domain, _| domain.to_s }.to_h
254
+ end
255
+
256
+ def determine_model_domain(model)
257
+ model_name = model.name.downcase
258
+
259
+ # Basic domain detection based on common patterns
260
+ return :auth if model_name.match?(/user|account|session|authentication|authorization/)
261
+ return :content if model_name.match?(/post|article|comment|blog|page|content/)
262
+ return :commerce if model_name.match?(/product|order|payment|cart|invoice|transaction/)
263
+ return :core if model_name.match?(/category|tag|setting|configuration|notification/)
264
+
265
+ # Default domain
266
+ :general
267
+ end
268
+
269
+ def create_domain_color_mapper(domains)
270
+ # Get colors from config or use defaults
271
+ colors = config[:default_colors] || DomainColorMapper::DEFAULT_COLORS
272
+ DomainColorMapper.new(domains, colors: colors)
273
+ end
274
+
275
+ def format_model_name(model)
276
+ return model.name unless config[:include_all_databases] || config[:show_database_labels]
277
+
278
+ # Get database name from the model's connection
279
+ begin
280
+ db_name = model.connection.pool.db_config.name
281
+ return model.name if db_name == 'primary' # Don't prefix primary database models
282
+
283
+ "#{model.name}[#{db_name}]"
284
+ rescue StandardError
285
+ model.name
286
+ end
287
+ end
288
+
289
+ def save_output(content, extension)
290
+ output_dir = config[:output_dir] || 'doc/erd'
291
+ FileUtils.mkdir_p(output_dir)
292
+
293
+ filename = File.join(output_dir, "erd.#{extension}")
294
+ File.write(filename, content)
295
+
296
+ Rails.logger.debug { "ERD saved to: #{filename}" }
297
+ filename # Return the filename
298
+ end
299
+ end
300
+ end
301
+ 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