rails_lens 0.5.3 → 0.5.4

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/lib/rails_lens/analyzers/association_analyzer.rb +0 -10
  4. data/lib/rails_lens/analyzers/base.rb +16 -0
  5. data/lib/rails_lens/analyzers/best_practices_analyzer.rb +0 -14
  6. data/lib/rails_lens/analyzers/callbacks.rb +11 -23
  7. data/lib/rails_lens/analyzers/composite_keys.rb +1 -1
  8. data/lib/rails_lens/analyzers/delegated_types.rb +1 -1
  9. data/lib/rails_lens/analyzers/enums.rb +1 -1
  10. data/lib/rails_lens/analyzers/error_handling.rb +11 -25
  11. data/lib/rails_lens/analyzers/inheritance.rb +4 -4
  12. data/lib/rails_lens/analyzers/notes.rb +0 -10
  13. data/lib/rails_lens/analyzers/performance_analyzer.rb +6 -30
  14. data/lib/rails_lens/annotation_pipeline.rb +14 -8
  15. data/lib/rails_lens/cli.rb +11 -22
  16. data/lib/rails_lens/cli_error_handler.rb +19 -20
  17. data/lib/rails_lens/commands.rb +30 -38
  18. data/lib/rails_lens/erd/visualizer.rb +15 -58
  19. data/lib/rails_lens/extensions/base.rb +1 -1
  20. data/lib/rails_lens/file_insertion_helper.rb +33 -6
  21. data/lib/rails_lens/mailer/annotator.rb +0 -16
  22. data/lib/rails_lens/mailer/extractor.rb +6 -8
  23. data/lib/rails_lens/model_detector.rb +27 -31
  24. data/lib/rails_lens/parsers/class_info.rb +1 -27
  25. data/lib/rails_lens/parsers/module_info.rb +1 -26
  26. data/lib/rails_lens/parsers/node_info.rb +34 -0
  27. data/lib/rails_lens/providers/view_provider.rb +1 -1
  28. data/lib/rails_lens/schema/adapters/base.rb +34 -1
  29. data/lib/rails_lens/schema/adapters/database_info.rb +1 -1
  30. data/lib/rails_lens/schema/adapters/mysql.rb +26 -80
  31. data/lib/rails_lens/schema/adapters/postgresql.rb +0 -19
  32. data/lib/rails_lens/schema/adapters/sqlite3.rb +0 -31
  33. data/lib/rails_lens/schema/annotation_manager.rb +72 -125
  34. data/lib/rails_lens/schema/annotation_removal.rb +24 -0
  35. data/lib/rails_lens/schema/database_annotator.rb +2 -15
  36. data/lib/rails_lens/toml_format.rb +14 -0
  37. data/lib/rails_lens/version.rb +1 -1
  38. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 792f89c06b8c11b77fb6892aa7488397d068104e2f30b7c343e38039660b0a2f
4
- data.tar.gz: d808455520cffa29f877e43ca8492a1e6dae238e231c641fe8ddd36663ba3d07
3
+ metadata.gz: 4e504fa26391d500a945947795688a4a52a23922e0ea6a760762415c698d91fc
4
+ data.tar.gz: 800c91d7df514dc2c745235cf9f5423a7a68d6499cee7221d5cb89adaa95157b
5
5
  SHA512:
6
- metadata.gz: a754d8353f7a75e11283af18fb2c128bdedec40bf673b5c2cae042d48c14d98dd3c7f26c118d81e0af09a836e0624a4ad2dbd5d092b7b64e4c8664aa9540df0f
7
- data.tar.gz: 821f26b9307bfd92914e9c1e6266c3bad0db90c16593e98eb91c1878f9662b3409485a0f32d03d8a007fd1d73bb9f114faec367ce533c41699f01cc2b0b12dc9
6
+ metadata.gz: 0c17d650e862adcf76e32d7a4a5398cd8c4827b8dd0e0424f01e307a76285e18001c5431561807a049695fbe3c4eaf68960add7d139427b1fcabb36ef2692e06
7
+ data.tar.gz: 31bf7b191ec9f5dfedabf351798d38b217a71b85c9ceba7e458ed7b45d61662b367989a40b3e311f9fc1e21f270eacbf55e6998e5b28ff0114fa0b5b37b9ab80
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.4](https://github.com/seuros/rails_lens/compare/rails_lens/v0.5.3...rails_lens/v0.5.4) (2026-06-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * prevent duplicate extension annotation blocks on re-annotate ([0e85197](https://github.com/seuros/rails_lens/commit/0e8519742c751eb26d225a3a9a8434a4da4053b1))
9
+
3
10
  ## [0.5.3](https://github.com/seuros/rails_lens/compare/rails_lens/v0.5.2...rails_lens/v0.5.3) (2026-02-17)
4
11
 
5
12
 
@@ -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]
@@ -29,7 +29,7 @@ module RailsLens
29
29
 
30
30
  def format_composite_keys(keys)
31
31
  lines = ['[composite_pk]']
32
- lines << "keys = [#{keys.map { |k| "\"#{k}\"" }.join(', ')}]"
32
+ lines << "keys = #{TomlFormat.quoted_array(keys)}"
33
33
  lines.join("\n")
34
34
  end
35
35
 
@@ -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
 
@@ -15,14 +15,7 @@ module RailsLens
15
15
  results = Schema::AnnotationManager.annotate_all(options)
16
16
 
17
17
  output.say "Annotated #{results[:annotated].length} models", :green
18
- output.say "Skipped #{results[:skipped].length} models", :yellow if results[:skipped].any?
19
-
20
- if results[:failed].any?
21
- output.say "Failed to annotate #{results[:failed].length} models:", :red
22
- results[:failed].each do |failure|
23
- output.say " - #{failure[:model]}: #{failure[:error]}", :red
24
- end
25
- end
18
+ report_skipped_and_failed(results, 'models')
26
19
 
27
20
  # Also annotate database-level objects (functions, etc.)
28
21
  if options[:include_database_objects]
@@ -37,36 +30,17 @@ module RailsLens
37
30
  results = Schema::DatabaseAnnotator.annotate_all(options)
38
31
 
39
32
  output.say "Annotated #{results[:annotated].length} abstract base classes with database objects", :green
40
- output.say "Skipped #{results[:skipped].length} abstract classes", :yellow if results[:skipped].any?
41
-
42
- if results[:failed].any?
43
- output.say "Failed to annotate #{results[:failed].length} abstract classes:", :red
44
- results[:failed].each do |failure|
45
- output.say " - #{failure[:model]}: #{failure[:error]}", :red
46
- end
47
- end
33
+ report_skipped_and_failed(results, 'abstract classes')
48
34
 
49
35
  results
50
36
  end
51
37
 
52
38
  def annotate_routes(options = {})
53
- annotator = Route::Annotator.new(dry_run: options[:dry_run])
54
- changed_files = annotator.annotate_all
55
-
56
- output.say "Annotated #{changed_files.length} controller files with routes", :green
57
- changed_files.each { |file| output.say " - #{file}", :blue } if options[:verbose] && changed_files.any?
58
-
59
- { changed_files: changed_files }
39
+ annotate_files(Route::Annotator.new(dry_run: options[:dry_run]), 'controller files with routes', options)
60
40
  end
61
41
 
62
42
  def annotate_mailers(options = {})
63
- annotator = Mailer::Annotator.new(dry_run: options[:dry_run])
64
- changed_files = annotator.annotate_all
65
-
66
- output.say "Annotated #{changed_files.length} mailer files", :green
67
- changed_files.each { |file| output.say " - #{file}", :blue } if options[:verbose] && changed_files.any?
68
-
69
- { changed_files: changed_files }
43
+ annotate_files(Mailer::Annotator.new(dry_run: options[:dry_run]), 'mailer files', options)
70
44
  end
71
45
 
72
46
  def remove_models(options = {})
@@ -76,17 +50,11 @@ module RailsLens
76
50
  end
77
51
 
78
52
  def remove_routes(options = {})
79
- annotator = Route::Annotator.new(dry_run: options[:dry_run])
80
- changed_files = annotator.remove_all
81
- output.say "Removed route annotations from #{changed_files.length} controller files", :green
82
- { changed_files: changed_files }
53
+ remove_files(Route::Annotator.new(dry_run: options[:dry_run]), 'route', 'controller files')
83
54
  end
84
55
 
85
56
  def remove_mailers(options = {})
86
- annotator = Mailer::Annotator.new(dry_run: options[:dry_run])
87
- changed_files = annotator.remove_all
88
- output.say "Removed mailer annotations from #{changed_files.length} mailer files", :green
89
- { changed_files: changed_files }
57
+ remove_files(Mailer::Annotator.new(dry_run: options[:dry_run]), 'mailer', 'mailer files')
90
58
  end
91
59
 
92
60
  def generate_erd(options = {})
@@ -227,6 +195,30 @@ module RailsLens
227
195
 
228
196
  private
229
197
 
198
+ # Report skipped/failed counts for a model-annotation results hash.
199
+ def report_skipped_and_failed(results, noun)
200
+ output.say "Skipped #{results[:skipped].length} #{noun}", :yellow if results[:skipped].any?
201
+ return unless results[:failed].any?
202
+
203
+ output.say "Failed to annotate #{results[:failed].length} #{noun}:", :red
204
+ results[:failed].each do |failure|
205
+ output.say " - #{failure[:model]}: #{failure[:error]}", :red
206
+ end
207
+ end
208
+
209
+ def annotate_files(annotator, label, options)
210
+ changed_files = annotator.annotate_all
211
+ output.say "Annotated #{changed_files.length} #{label}", :green
212
+ changed_files.each { |file| output.say " - #{file}", :blue } if options[:verbose] && changed_files.any?
213
+ { changed_files: changed_files }
214
+ end
215
+
216
+ def remove_files(annotator, kind, target)
217
+ changed_files = annotator.remove_all
218
+ output.say "Removed #{kind} annotations from #{changed_files.length} #{target}", :green
219
+ { changed_files: changed_files }
220
+ end
221
+
230
222
  def rake_task_template
231
223
  <<~RAKE
232
224
  # frozen_string_literal: true