rails_lens 0.5.3 → 0.10.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +1 -1
  4. data/lib/rails_lens/analyzers/association_analyzer.rb +0 -10
  5. data/lib/rails_lens/analyzers/base.rb +16 -0
  6. data/lib/rails_lens/analyzers/best_practices_analyzer.rb +0 -14
  7. data/lib/rails_lens/analyzers/callbacks.rb +11 -23
  8. data/lib/rails_lens/analyzers/composite_keys.rb +4 -38
  9. data/lib/rails_lens/analyzers/delegated_types.rb +1 -1
  10. data/lib/rails_lens/analyzers/enums.rb +1 -1
  11. data/lib/rails_lens/analyzers/error_handling.rb +11 -25
  12. data/lib/rails_lens/analyzers/inheritance.rb +4 -4
  13. data/lib/rails_lens/analyzers/notes.rb +0 -10
  14. data/lib/rails_lens/analyzers/performance_analyzer.rb +6 -30
  15. data/lib/rails_lens/annotation_pipeline.rb +14 -8
  16. data/lib/rails_lens/cli.rb +11 -22
  17. data/lib/rails_lens/cli_error_handler.rb +19 -20
  18. data/lib/rails_lens/commands.rb +33 -41
  19. data/lib/rails_lens/erd/column_type_formatter.rb +6 -1
  20. data/lib/rails_lens/erd/visualizer.rb +57 -98
  21. data/lib/rails_lens/errors.rb +3 -3
  22. data/lib/rails_lens/extensions/base.rb +1 -1
  23. data/lib/rails_lens/file_insertion_helper.rb +33 -6
  24. data/lib/rails_lens/mailer/annotator.rb +1 -16
  25. data/lib/rails_lens/mailer/extractor.rb +6 -8
  26. data/lib/rails_lens/model_detector.rb +27 -31
  27. data/lib/rails_lens/parsers/class_info.rb +1 -27
  28. data/lib/rails_lens/parsers/module_info.rb +1 -26
  29. data/lib/rails_lens/parsers/node_info.rb +34 -0
  30. data/lib/rails_lens/providers/view_provider.rb +1 -1
  31. data/lib/rails_lens/route/annotator.rb +1 -0
  32. data/lib/rails_lens/schema/adapters/base.rb +38 -4
  33. data/lib/rails_lens/schema/adapters/database_info.rb +1 -1
  34. data/lib/rails_lens/schema/adapters/mysql.rb +26 -80
  35. data/lib/rails_lens/schema/adapters/postgresql.rb +3 -22
  36. data/lib/rails_lens/schema/adapters/sqlite3.rb +0 -31
  37. data/lib/rails_lens/schema/annotation_manager.rb +88 -127
  38. data/lib/rails_lens/schema/annotation_removal.rb +24 -0
  39. data/lib/rails_lens/schema/database_annotator.rb +2 -15
  40. data/lib/rails_lens/tasks/annotate.rake +6 -1
  41. data/lib/rails_lens/toml_format.rb +14 -0
  42. data/lib/rails_lens/version.rb +1 -1
  43. data/lib/rails_lens.rb +4 -0
  44. metadata +12 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 792f89c06b8c11b77fb6892aa7488397d068104e2f30b7c343e38039660b0a2f
4
- data.tar.gz: d808455520cffa29f877e43ca8492a1e6dae238e231c641fe8ddd36663ba3d07
3
+ metadata.gz: 0f010a15d0d3afdf64754e866cac571fcb1c91e4bd3981c8aa9bdece8d30f85a
4
+ data.tar.gz: 11e8b6ad7da6461bdc846952151537d3495d67cd812cd9f7e4b3ca40dbc37fbb
5
5
  SHA512:
6
- metadata.gz: a754d8353f7a75e11283af18fb2c128bdedec40bf673b5c2cae042d48c14d98dd3c7f26c118d81e0af09a836e0624a4ad2dbd5d092b7b64e4c8664aa9540df0f
7
- data.tar.gz: 821f26b9307bfd92914e9c1e6266c3bad0db90c16593e98eb91c1878f9662b3409485a0f32d03d8a007fd1d73bb9f114faec367ce533c41699f01cc2b0b12dc9
6
+ metadata.gz: 22963fce37978c1a4157d9c1ffe236a3ca41c0a97d8b492a832729b467c91923810cc70126c4717c17cbbac9596dfbcd620a34cdb482092b34a8692a37351c23
7
+ data.tar.gz: 4c5bd10154b4d44b4dfda08ec21ebcd3632b6a740e7a9d28ddd7d5a35289fec248fcf76b61bc19db8a53f548691a5597df326bda48f25ad0cc200c94468f94bf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.0](https://github.com/seuros/rails_lens/compare/rails_lens/v0.5.4...rails_lens/v0.10.0) (2026-08-10)
4
+
5
+
6
+ ### Features
7
+
8
+ * composite primary keys ([#45](https://github.com/seuros/rails_lens/issues/45)) ([8d934b5](https://github.com/seuros/rails_lens/commit/8d934b51b310252ce42f3db9aa85894161d562f3))
9
+
10
+ ## [0.5.4](https://github.com/seuros/rails_lens/compare/rails_lens/v0.5.3...rails_lens/v0.5.4) (2026-06-26)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * prevent duplicate extension annotation blocks on re-annotate ([0e85197](https://github.com/seuros/rails_lens/commit/0e8519742c751eb26d225a3a9a8434a4da4053b1))
16
+
3
17
  ## [0.5.3](https://github.com/seuros/rails_lens/compare/rails_lens/v0.5.2...rails_lens/v0.5.3) (2026-02-17)
4
18
 
5
19
 
data/README.md CHANGED
@@ -120,7 +120,7 @@ erDiagram
120
120
  ## Requirements
121
121
 
122
122
  - Ruby >= 3.4.0
123
- - Rails >= 7.2.0
123
+ - Rails >= 8.0.0
124
124
 
125
125
  ## Installation
126
126
 
@@ -71,16 +71,6 @@ module RailsLens
71
71
  false
72
72
  end
73
73
 
74
- def needs_explicit_inverse_of?(association)
75
- # Rails can auto-infer inverse_of for vanilla associations
76
- # Only require explicit inverse_of when using custom options
77
- association.options[:class_name].present? ||
78
- association.options[:foreign_key].present? ||
79
- association.options[:as].present? ||
80
- association.options[:source].present? ||
81
- association.options[:through].present?
82
- end
83
-
84
74
  def should_have_counter_cache?(association)
85
75
  return false unless association.macro == :belongs_to
86
76
 
@@ -30,6 +30,22 @@ module RailsLens
30
30
  def adapter_name
31
31
  @adapter_name ||= connection.adapter_name
32
32
  end
33
+
34
+ def indexed?(column)
35
+ connection.indexes(table_name).any? do |index|
36
+ index.columns.include?(column.name)
37
+ end
38
+ end
39
+
40
+ def needs_explicit_inverse_of?(association)
41
+ # Rails can auto-infer inverse_of for vanilla associations
42
+ # Only require explicit inverse_of when using custom options
43
+ association.options[:class_name].present? ||
44
+ association.options[:foreign_key].present? ||
45
+ association.options[:as].present? ||
46
+ association.options[:source].present? ||
47
+ association.options[:through].present?
48
+ end
33
49
  end
34
50
  end
35
51
  end
@@ -71,23 +71,9 @@ module RailsLens
71
71
  model_class.column_names.include?(name)
72
72
  end
73
73
 
74
- def indexed?(column)
75
- connection.indexes(table_name).any? do |index|
76
- index.columns.include?(column.name)
77
- end
78
- end
79
-
80
74
  def columns
81
75
  @columns ||= model_class.columns
82
76
  end
83
-
84
- def connection
85
- model_class.connection
86
- end
87
-
88
- def table_name
89
- model_class.table_name
90
- end
91
77
  end
92
78
  end
93
79
  end
@@ -178,18 +178,13 @@ module RailsLens
178
178
  def extract_options(callback)
179
179
  options = {}
180
180
 
181
- # Extract :if condition
182
- if callback.instance_variable_defined?(:@if) && callback.instance_variable_get(:@if).present?
183
- conditions = callback.instance_variable_get(:@if)
184
- formatted = format_conditions(conditions)
185
- options[:if] = formatted if formatted.any?
186
- end
181
+ # Extract :if / :unless conditions (stored as matching instance variables)
182
+ %i[if unless].each do |key|
183
+ ivar = :"@#{key}"
184
+ next unless callback.instance_variable_defined?(ivar) && callback.instance_variable_get(ivar).present?
187
185
 
188
- # Extract :unless condition
189
- if callback.instance_variable_defined?(:@unless) && callback.instance_variable_get(:@unless).present?
190
- conditions = callback.instance_variable_get(:@unless)
191
- formatted = format_conditions(conditions)
192
- options[:unless] = formatted if formatted.any?
186
+ formatted = format_conditions(callback.instance_variable_get(ivar))
187
+ options[key] = formatted if formatted.any?
193
188
  end
194
189
 
195
190
  # Extract :on option (for validation and commit callbacks)
@@ -274,19 +269,12 @@ module RailsLens
274
269
  parts = []
275
270
  parts << "method = \"#{escape_toml(callback[:method])}\""
276
271
 
277
- if callback[:options][:if]&.any?
278
- if_values = callback[:options][:if].map { |v| "\"#{escape_toml(v)}\"" }.join(', ')
279
- parts << "if = [#{if_values}]"
280
- end
281
-
282
- if callback[:options][:unless]&.any?
283
- unless_values = callback[:options][:unless].map { |v| "\"#{escape_toml(v)}\"" }.join(', ')
284
- parts << "unless = [#{unless_values}]"
285
- end
272
+ %i[if unless on].each do |key|
273
+ values = callback[:options][key]
274
+ next unless values&.any?
286
275
 
287
- if callback[:options][:on]&.any?
288
- on_values = callback[:options][:on].map { |v| "\"#{escape_toml(v)}\"" }.join(', ')
289
- parts << "on = [#{on_values}]"
276
+ escaped = values.map { |v| "\"#{escape_toml(v)}\"" }.join(', ')
277
+ parts << "#{key} = [#{escaped}]"
290
278
  end
291
279
 
292
280
  parts << 'prepend = true' if callback[:options][:prepend]
@@ -4,22 +4,10 @@ module RailsLens
4
4
  module Analyzers
5
5
  class CompositeKeys < Base
6
6
  def analyze
7
- # First try Rails native support
8
- if model_class.respond_to?(:primary_keys) && model_class.primary_keys.is_a?(Array)
9
- keys = model_class.primary_keys
10
- return format_composite_keys(keys) if keys.length > 1
11
- end
7
+ pk = model_class.primary_key
8
+ return nil unless pk.is_a?(Array)
12
9
 
13
- # For PostgreSQL, check the actual database constraints
14
- if adapter_name == 'PostgreSQL'
15
- keys = detect_composite_primary_key_from_db
16
- return format_composite_keys(keys) if keys && keys.length > 1
17
- end
18
-
19
- nil
20
- rescue NoMethodError => e
21
- RailsLens.logger.debug { "Failed to analyze composite keys for #{model_class.name}: #{e.message}" }
22
- nil
10
+ format_composite_keys(pk)
23
11
  rescue ActiveRecord::ConnectionNotEstablished => e
24
12
  RailsLens.logger.debug { "No database connection for #{model_class.name}: #{e.message}" }
25
13
  nil
@@ -29,31 +17,9 @@ module RailsLens
29
17
 
30
18
  def format_composite_keys(keys)
31
19
  lines = ['[composite_pk]']
32
- lines << "keys = [#{keys.map { |k| "\"#{k}\"" }.join(', ')}]"
20
+ lines << "keys = #{TomlFormat.quoted_array(keys)}"
33
21
  lines.join("\n")
34
22
  end
35
-
36
- def detect_composite_primary_key_from_db
37
- # Query PostgreSQL system catalogs to find composite primary keys
38
- sql = <<~SQL.squish
39
- SELECT a.attname
40
- FROM pg_index i
41
- JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
42
- WHERE i.indrelid = '#{table_name}'::regclass
43
- AND i.indisprimary
44
- ORDER BY array_position(i.indkey, a.attnum)
45
- SQL
46
-
47
- result = connection.execute(sql)
48
- keys = result.pluck('attname')
49
- keys.empty? ? nil : keys
50
- rescue ActiveRecord::StatementInvalid => e
51
- RailsLens.logger.debug { "Failed to detect composite keys from database for #{table_name}: #{e.message}" }
52
- nil
53
- rescue PG::Error => e
54
- RailsLens.logger.debug { "PostgreSQL error detecting composite keys: #{e.message}" }
55
- nil
56
- end
57
23
  end
58
24
  end
59
25
  end
@@ -19,7 +19,7 @@ module RailsLens
19
19
  else
20
20
  Array(delegated_type_info[:types])
21
21
  end
22
- lines << "types = [#{types_list.map { |t| "\"#{t}\"" }.join(', ')}]"
22
+ lines << "types = #{TomlFormat.quoted_array(types_list)}"
23
23
 
24
24
  lines.join("\n")
25
25
  end
@@ -11,7 +11,7 @@ module RailsLens
11
11
 
12
12
  model_class.defined_enums.each do |name, values|
13
13
  # Format as TOML inline table: name = { key = "value", ... }
14
- formatted_values = if values.values.all? { |v| v.is_a?(Integer) }
14
+ formatted_values = if values.values.all?(Integer)
15
15
  # Integer-based enum
16
16
  values.map { |k, v| "#{k} = #{v}" }.join(', ')
17
17
  else
@@ -16,49 +16,35 @@ module RailsLens
16
16
 
17
17
  private
18
18
 
19
+ # Report an analyzer error with the shared analyzer/model context plus any
20
+ # call-specific +context+ keys.
21
+ def report_analyzer_error(error, **context)
22
+ ErrorReporter.report(error, { analyzer: self.class.name, model: model_class.name }.merge(context))
23
+ end
24
+
19
25
  def handle_database_error(error)
20
- ErrorReporter.report(error, {
21
- analyzer: self.class.name,
22
- model: model_class.name,
23
- table: model_class.table_name
24
- })
26
+ report_analyzer_error(error, table: model_class.table_name)
25
27
  []
26
28
  end
27
29
 
28
30
  def handle_method_error(error)
29
31
  # These are likely bugs in our code, so we should log them prominently
30
- ErrorReporter.report(error, {
31
- analyzer: self.class.name,
32
- model: model_class.name,
33
- method: error.name
34
- })
32
+ report_analyzer_error(error, method: error.name)
35
33
  []
36
34
  end
37
35
 
38
36
  def handle_unexpected_error(error)
39
- ErrorReporter.report(error, {
40
- analyzer: self.class.name,
41
- model: model_class.name,
42
- type: 'unexpected'
43
- })
37
+ report_analyzer_error(error, type: 'unexpected')
44
38
  []
45
39
  end
46
40
 
47
41
  def safe_call(default = nil)
48
42
  yield
49
43
  rescue ActiveRecord::StatementInvalid => e
50
- ErrorReporter.report(e, {
51
- analyzer: self.class.name,
52
- model: model_class.name,
53
- operation: 'database_query'
54
- })
44
+ report_analyzer_error(e, operation: 'database_query')
55
45
  default
56
46
  rescue NoMethodError, NameError => e
57
- ErrorReporter.report(e, {
58
- analyzer: self.class.name,
59
- model: model_class.name,
60
- operation: 'method_call'
61
- })
47
+ report_analyzer_error(e, operation: 'method_call')
62
48
  default
63
49
  end
64
50
  end
@@ -46,7 +46,7 @@ module RailsLens
46
46
  if model_class.base_class == model_class
47
47
  # This is the base class
48
48
  subclasses = find_sti_subclasses
49
- lines << "subclasses = [#{subclasses.map { |s| "\"#{s}\"" }.join(', ')}]" if subclasses.any?
49
+ lines << "subclasses = #{TomlFormat.quoted_array(subclasses)}" if subclasses.any?
50
50
  lines << 'base = true'
51
51
  else
52
52
  # This is a subclass
@@ -55,7 +55,7 @@ module RailsLens
55
55
 
56
56
  # Find siblings
57
57
  siblings = find_sti_siblings
58
- lines << "siblings = [#{siblings.map { |s| "\"#{s}\"" }.join(', ')}]" if siblings.any?
58
+ lines << "siblings = #{TomlFormat.quoted_array(siblings)}" if siblings.any?
59
59
  end
60
60
 
61
61
  lines.join("\n")
@@ -73,7 +73,7 @@ module RailsLens
73
73
 
74
74
  # Try to find known types
75
75
  types = find_delegated_types(reflection)
76
- lines << "types = [#{types.map { |t| "\"#{t}\"" }.join(', ')}]" if types.any?
76
+ lines << "types = #{TomlFormat.quoted_array(types)}" if types.any?
77
77
 
78
78
  lines.join("\n")
79
79
  end
@@ -163,7 +163,7 @@ module RailsLens
163
163
  []
164
164
  end
165
165
  if types.any?
166
- "{ name = \"#{reflection.name}\", type_col = \"#{reflection.foreign_type}\", id_col = \"#{reflection.foreign_key}\", types = [#{types.map { |t| "\"#{t}\"" }.join(', ')}] }"
166
+ "{ name = \"#{reflection.name}\", type_col = \"#{reflection.foreign_type}\", id_col = \"#{reflection.foreign_key}\", types = #{TomlFormat.quoted_array(types)} }"
167
167
  else
168
168
  "{ name = \"#{reflection.name}\", type_col = \"#{reflection.foreign_type}\", id_col = \"#{reflection.foreign_key}\" }"
169
169
  end
@@ -417,16 +417,6 @@ module RailsLens
417
417
  RailsLens.logger.debug { "Error checking view existence for #{view_name}: #{e.message}" }
418
418
  false
419
419
  end
420
-
421
- def needs_explicit_inverse_of?(association)
422
- # Rails can auto-infer inverse_of for vanilla associations
423
- # Only require explicit inverse_of when using custom options
424
- association.options[:class_name].present? ||
425
- association.options[:foreign_key].present? ||
426
- association.options[:as].present? ||
427
- association.options[:source].present? ||
428
- association.options[:through].present?
429
- end
430
420
  end
431
421
  end
432
422
  end
@@ -27,23 +27,13 @@ module RailsLens
27
27
  end
28
28
 
29
29
  def analyze_query_performance
30
- notes = []
31
-
32
- # Check for columns that are commonly used in WHERE clauses
33
- commonly_queried_columns.each do |column|
34
- next if indexed?(column)
35
-
36
- notes << NoteCodes.note(column.name, NoteCodes::INDEX)
37
- end
38
-
39
- # Check for missing indexes on scoped columns
40
- scoped_columns.each do |column|
41
- next if indexed?(column)
42
-
43
- notes << NoteCodes.note(column.name, NoteCodes::INDEX)
44
- end
30
+ # Flag commonly-queried and scoped columns that lack an index
31
+ index_notes_for(commonly_queried_columns) + index_notes_for(scoped_columns)
32
+ end
45
33
 
46
- notes
34
+ def index_notes_for(columns)
35
+ columns.reject { |column| indexed?(column) }
36
+ .map { |column| NoteCodes.note(column.name, NoteCodes::INDEX) }
47
37
  end
48
38
 
49
39
  def uuid_columns
@@ -68,20 +58,6 @@ module RailsLens
68
58
  column.name.end_with?('_id')
69
59
  end
70
60
  end
71
-
72
- def indexed?(column)
73
- connection.indexes(table_name).any? do |index|
74
- index.columns.include?(column.name)
75
- end
76
- end
77
-
78
- def connection
79
- model_class.connection
80
- end
81
-
82
- def table_name
83
- model_class.table_name
84
- end
85
61
  end
86
62
  end
87
63
  end
@@ -20,6 +20,19 @@ module RailsLens
20
20
 
21
21
  delegate :clear, to: :@providers
22
22
 
23
+ # Route a single provider's result into the accumulator hash based on the
24
+ # provider's declared type. Shared by the pipeline and AnnotationManager.
25
+ def self.accumulate_result(results, provider, result)
26
+ case provider.type
27
+ when :schema
28
+ results[:schema] = result
29
+ when :section
30
+ results[:sections] << result if result
31
+ when :notes
32
+ results[:notes].concat(Array(result))
33
+ end
34
+ end
35
+
23
36
  def process(model_class)
24
37
  results = {
25
38
  schema: nil,
@@ -35,14 +48,7 @@ module RailsLens
35
48
  begin
36
49
  result = provider.process(model_class, connection)
37
50
 
38
- case provider.type
39
- when :schema
40
- results[:schema] = result
41
- when :section
42
- results[:sections] << result if result
43
- when :notes
44
- results[:notes].concat(Array(result))
45
- end
51
+ self.class.accumulate_result(results, provider, result)
46
52
  rescue ActiveRecord::StatementInvalid => e
47
53
  warn "Provider #{provider.class} database error for #{model_class}: #{e.message}"
48
54
  rescue ActiveRecord::ConnectionNotDefined => e
@@ -34,13 +34,13 @@ module RailsLens
34
34
  commands = Commands.new(self)
35
35
 
36
36
  # Annotate models (default behavior or when --all is specified)
37
- results[:models] = commands.annotate_models(options) if should_annotate_models?
37
+ results[:models] = commands.annotate_models(options) if target_models?
38
38
 
39
39
  # Annotate routes
40
- results[:routes] = commands.annotate_routes(options) if should_annotate_routes?
40
+ results[:routes] = commands.annotate_routes(options) if target_routes?
41
41
 
42
42
  # Annotate mailers
43
- results[:mailers] = commands.annotate_mailers(options) if should_annotate_mailers?
43
+ results[:mailers] = commands.annotate_mailers(options) if target_mailers?
44
44
 
45
45
  results
46
46
  end
@@ -58,13 +58,13 @@ module RailsLens
58
58
  commands = Commands.new(self)
59
59
 
60
60
  # Remove model annotations (default behavior or when --all is specified)
61
- results[:models] = commands.remove_models(options) if should_remove_models?
61
+ results[:models] = commands.remove_models(options) if target_models?
62
62
 
63
63
  # Remove route annotations
64
- results[:routes] = commands.remove_routes(options) if should_remove_routes?
64
+ results[:routes] = commands.remove_routes(options) if target_routes?
65
65
 
66
66
  # Remove mailer annotations
67
- results[:mailers] = commands.remove_mailers(options) if should_remove_mailers?
67
+ results[:mailers] = commands.remove_mailers(options) if target_mailers?
68
68
 
69
69
  results
70
70
  end
@@ -158,28 +158,17 @@ module RailsLens
158
158
  RailsLens.config.debug = options[:debug]
159
159
  end
160
160
 
161
- # Helper methods to determine what to annotate/remove
162
- def should_annotate_models?
161
+ # Helper methods to determine what to annotate/remove. The same targeting
162
+ # rules apply to both the annotate and remove commands.
163
+ def target_models?
163
164
  (!options[:routes] && !options[:mailers]) || options[:all]
164
165
  end
165
166
 
166
- def should_annotate_routes?
167
+ def target_routes?
167
168
  options[:routes] || options[:all]
168
169
  end
169
170
 
170
- def should_annotate_mailers?
171
- options[:mailers] || options[:all]
172
- end
173
-
174
- def should_remove_models?
175
- (!options[:routes] && !options[:mailers]) || options[:all]
176
- end
177
-
178
- def should_remove_routes?
179
- options[:routes] || options[:all]
180
- end
181
-
182
- def should_remove_mailers?
171
+ def target_mailers?
183
172
  options[:mailers] || options[:all]
184
173
  end
185
174
  end
@@ -31,32 +31,31 @@ module RailsLens
31
31
  end
32
32
 
33
33
  def handle_model_error(error)
34
- say "Model Error: #{error.message}", :red
35
- if options[:verbose]
36
- say 'Make sure your Rails application is properly loaded', :yellow
37
- say 'Try running: bundle exec rails_lens annotate', :yellow
38
- end
39
- exit 1
34
+ report_error('Model Error', error,
35
+ 'Make sure your Rails application is properly loaded',
36
+ 'Try running: bundle exec rails_lens annotate')
40
37
  end
41
38
 
42
39
  def handle_database_error(error)
43
- say "Database Error: #{error.message}", :red
44
- if options[:verbose]
45
- say 'Possible causes:', :yellow
46
- say ' - Database server is not running', :yellow
47
- say ' - Invalid database credentials', :yellow
48
- say ' - Table does not exist', :yellow
49
- say ' - Permission denied', :yellow
50
- end
51
- exit 1
40
+ report_error('Database Error', error,
41
+ 'Possible causes:',
42
+ ' - Database server is not running',
43
+ ' - Invalid database credentials',
44
+ ' - Table does not exist',
45
+ ' - Permission denied')
52
46
  end
53
47
 
54
48
  def handle_annotation_error(error)
55
- say "Annotation Error: #{error.message}", :red
56
- if options[:verbose]
57
- say 'Failed to annotate one or more files', :yellow
58
- say 'Check file permissions and syntax', :yellow
59
- end
49
+ report_error('Annotation Error', error,
50
+ 'Failed to annotate one or more files',
51
+ 'Check file permissions and syntax')
52
+ end
53
+
54
+ # Print a red "<label>: <message>", optional verbose-only yellow hints,
55
+ # then exit non-zero.
56
+ def report_error(label, error, *verbose_hints)
57
+ say "#{label}: #{error.message}", :red
58
+ verbose_hints.each { |hint| say hint, :yellow } if options[:verbose]
60
59
  exit 1
61
60
  end
62
61