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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/LICENSE.txt +2 -2
- data/README.md +463 -9
- data/exe/rails_lens +25 -0
- data/lib/rails_lens/analyzers/association_analyzer.rb +111 -0
- data/lib/rails_lens/analyzers/base.rb +35 -0
- data/lib/rails_lens/analyzers/best_practices_analyzer.rb +114 -0
- data/lib/rails_lens/analyzers/column_analyzer.rb +97 -0
- data/lib/rails_lens/analyzers/composite_keys.rb +62 -0
- data/lib/rails_lens/analyzers/database_constraints.rb +35 -0
- data/lib/rails_lens/analyzers/delegated_types.rb +129 -0
- data/lib/rails_lens/analyzers/enums.rb +34 -0
- data/lib/rails_lens/analyzers/error_handling.rb +66 -0
- data/lib/rails_lens/analyzers/foreign_key_analyzer.rb +47 -0
- data/lib/rails_lens/analyzers/generated_columns.rb +56 -0
- data/lib/rails_lens/analyzers/index_analyzer.rb +128 -0
- data/lib/rails_lens/analyzers/inheritance.rb +212 -0
- data/lib/rails_lens/analyzers/notes.rb +325 -0
- data/lib/rails_lens/analyzers/performance_analyzer.rb +110 -0
- data/lib/rails_lens/annotation_pipeline.rb +87 -0
- data/lib/rails_lens/cli.rb +176 -0
- data/lib/rails_lens/cli_error_handler.rb +86 -0
- data/lib/rails_lens/commands.rb +164 -0
- data/lib/rails_lens/connection.rb +133 -0
- data/lib/rails_lens/erd/column_type_formatter.rb +32 -0
- data/lib/rails_lens/erd/domain_color_mapper.rb +40 -0
- data/lib/rails_lens/erd/mysql_column_type_formatter.rb +19 -0
- data/lib/rails_lens/erd/postgresql_column_type_formatter.rb +19 -0
- data/lib/rails_lens/erd/visualizer.rb +329 -0
- data/lib/rails_lens/errors.rb +78 -0
- data/lib/rails_lens/extension_loader.rb +261 -0
- data/lib/rails_lens/extensions/base.rb +194 -0
- data/lib/rails_lens/extensions/closure_tree_ext.rb +157 -0
- data/lib/rails_lens/file_insertion_helper.rb +168 -0
- data/lib/rails_lens/mailer/annotator.rb +226 -0
- data/lib/rails_lens/mailer/extractor.rb +201 -0
- data/lib/rails_lens/model_detector.rb +252 -0
- data/lib/rails_lens/parsers/class_info.rb +46 -0
- data/lib/rails_lens/parsers/module_info.rb +33 -0
- data/lib/rails_lens/parsers/parser_result.rb +55 -0
- data/lib/rails_lens/parsers/prism_parser.rb +90 -0
- data/lib/rails_lens/parsers.rb +10 -0
- data/lib/rails_lens/providers/association_notes_provider.rb +11 -0
- data/lib/rails_lens/providers/base.rb +37 -0
- data/lib/rails_lens/providers/best_practices_notes_provider.rb +11 -0
- data/lib/rails_lens/providers/column_notes_provider.rb +11 -0
- data/lib/rails_lens/providers/composite_keys_provider.rb +11 -0
- data/lib/rails_lens/providers/database_constraints_provider.rb +11 -0
- data/lib/rails_lens/providers/delegated_types_provider.rb +11 -0
- data/lib/rails_lens/providers/enums_provider.rb +11 -0
- data/lib/rails_lens/providers/extension_notes_provider.rb +20 -0
- data/lib/rails_lens/providers/extensions_provider.rb +22 -0
- data/lib/rails_lens/providers/foreign_key_notes_provider.rb +11 -0
- data/lib/rails_lens/providers/generated_columns_provider.rb +11 -0
- data/lib/rails_lens/providers/index_notes_provider.rb +20 -0
- data/lib/rails_lens/providers/inheritance_provider.rb +23 -0
- data/lib/rails_lens/providers/notes_provider_base.rb +25 -0
- data/lib/rails_lens/providers/performance_notes_provider.rb +11 -0
- data/lib/rails_lens/providers/schema_provider.rb +61 -0
- data/lib/rails_lens/providers/section_provider_base.rb +28 -0
- data/lib/rails_lens/railtie.rb +17 -0
- data/lib/rails_lens/rake_bootstrapper.rb +18 -0
- data/lib/rails_lens/route/annotator.rb +268 -0
- data/lib/rails_lens/route/extractor.rb +133 -0
- data/lib/rails_lens/route/parser.rb +59 -0
- data/lib/rails_lens/schema/adapters/base.rb +345 -0
- data/lib/rails_lens/schema/adapters/database_info.rb +118 -0
- data/lib/rails_lens/schema/adapters/mysql.rb +279 -0
- data/lib/rails_lens/schema/adapters/postgresql.rb +197 -0
- data/lib/rails_lens/schema/adapters/sqlite3.rb +96 -0
- data/lib/rails_lens/schema/annotation.rb +144 -0
- data/lib/rails_lens/schema/annotation_manager.rb +202 -0
- data/lib/rails_lens/tasks/annotate.rake +35 -0
- data/lib/rails_lens/tasks/erd.rake +24 -0
- data/lib/rails_lens/tasks/mailers.rake +27 -0
- data/lib/rails_lens/tasks/routes.rake +27 -0
- data/lib/rails_lens/tasks/schema.rake +108 -0
- data/lib/rails_lens/version.rb +5 -0
- data/lib/rails_lens.rb +138 -5
- metadata +215 -11
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
module Schema
|
5
|
+
class Annotation
|
6
|
+
MARKER_FORMAT = 'rails-lens:schema'
|
7
|
+
|
8
|
+
attr_reader :content
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@content = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def start_marker
|
15
|
+
"# <#{MARKER_FORMAT}:begin>"
|
16
|
+
end
|
17
|
+
|
18
|
+
def end_marker
|
19
|
+
"# <#{MARKER_FORMAT}:end>"
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_line(line)
|
23
|
+
@content << line
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_lines(lines)
|
27
|
+
@content.concat(lines)
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
return '' if @content.empty?
|
32
|
+
|
33
|
+
lines = []
|
34
|
+
lines << start_marker
|
35
|
+
|
36
|
+
@content.each do |line|
|
37
|
+
lines << (line.empty? ? '#' : "# #{line}")
|
38
|
+
end
|
39
|
+
|
40
|
+
lines << end_marker
|
41
|
+
lines.join("\n")
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.extract(file_content)
|
45
|
+
start_marker = "# <#{MARKER_FORMAT}:begin>"
|
46
|
+
end_marker = "# <#{MARKER_FORMAT}:end>"
|
47
|
+
|
48
|
+
start_index = file_content.index(start_marker)
|
49
|
+
return nil unless start_index
|
50
|
+
|
51
|
+
end_index = file_content.index(end_marker, start_index)
|
52
|
+
return nil unless end_index
|
53
|
+
|
54
|
+
{
|
55
|
+
start_index: start_index,
|
56
|
+
end_index: end_index + end_marker.length,
|
57
|
+
content: file_content[start_index..(end_index + end_marker.length - 1)]
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.remove(file_content)
|
62
|
+
# Remove all occurrences of the annotation blocks
|
63
|
+
result = file_content.dup
|
64
|
+
|
65
|
+
while (annotation = extract(result))
|
66
|
+
# Preserve one newline if the annotation was followed by a newline
|
67
|
+
replacement = ''
|
68
|
+
replacement = "\n" if annotation[:end_index] < result.length && result[annotation[:end_index]] == "\n"
|
69
|
+
|
70
|
+
result[annotation[:start_index]...annotation[:end_index]] = replacement
|
71
|
+
end
|
72
|
+
|
73
|
+
# Clean up multiple consecutive blank lines
|
74
|
+
result.gsub(/\n\n\n+/, "\n\n")
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.insert_after_line(file_content, line_pattern, annotation_text)
|
78
|
+
lines = file_content.split("\n")
|
79
|
+
insert_index = nil
|
80
|
+
|
81
|
+
lines.each_with_index do |line, index|
|
82
|
+
if line.match?(line_pattern)
|
83
|
+
insert_index = index + 1
|
84
|
+
break
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
return file_content unless insert_index
|
89
|
+
|
90
|
+
# Insert the annotation
|
91
|
+
lines.insert(insert_index, annotation_text)
|
92
|
+
lines.join("\n")
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.update_or_insert(file_content, annotation_text, line_pattern)
|
96
|
+
# First, try to find and update existing annotation
|
97
|
+
if (existing = extract(file_content))
|
98
|
+
# Replace existing annotation
|
99
|
+
before = file_content[0...existing[:start_index]]
|
100
|
+
after = file_content[existing[:end_index]..]
|
101
|
+
|
102
|
+
# Ensure proper spacing
|
103
|
+
before = "#{before.rstrip}\n"
|
104
|
+
after = "\n#{after.lstrip}" if after && !after.start_with?("\n")
|
105
|
+
|
106
|
+
return before + annotation_text + after
|
107
|
+
end
|
108
|
+
|
109
|
+
# No existing annotation, insert new one
|
110
|
+
insert_after_line(file_content, line_pattern, annotation_text)
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.parse_content(annotation_block)
|
114
|
+
return {} unless annotation_block
|
115
|
+
|
116
|
+
lines = annotation_block.split("\n").map { |line| line.sub(/^#\s?/, '') }
|
117
|
+
|
118
|
+
# Remove marker lines
|
119
|
+
lines = lines[1..-2] if lines.first&.match?(/<.*:begin>/) && lines.last&.match?(/<.*:end>/)
|
120
|
+
|
121
|
+
sections = {}
|
122
|
+
current_section = nil
|
123
|
+
current_content = []
|
124
|
+
|
125
|
+
lines.each do |line|
|
126
|
+
if line.match?(/^==\s+(.+)/)
|
127
|
+
# Save previous section if any
|
128
|
+
sections[current_section] = current_content.join("\n").strip if current_section
|
129
|
+
|
130
|
+
current_section = line.strip
|
131
|
+
current_content = []
|
132
|
+
else
|
133
|
+
current_content << line
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Save last section
|
138
|
+
sections[current_section] = current_content.join("\n").strip if current_section
|
139
|
+
|
140
|
+
sections
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../file_insertion_helper'
|
4
|
+
|
5
|
+
module RailsLens
|
6
|
+
module Schema
|
7
|
+
class AnnotationManager
|
8
|
+
attr_reader :model_class
|
9
|
+
|
10
|
+
def initialize(model_class)
|
11
|
+
@model_class = model_class
|
12
|
+
end
|
13
|
+
|
14
|
+
def annotate_file(file_path = nil, allow_external_files: false)
|
15
|
+
file_path ||= model_file_path
|
16
|
+
return unless file_path && File.exist?(file_path)
|
17
|
+
|
18
|
+
# Only annotate files within the Rails application (unless explicitly allowed)
|
19
|
+
if !allow_external_files && defined?(Rails.root) && !file_path.start_with?(Rails.root.to_s)
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
annotation_text = generate_annotation
|
24
|
+
|
25
|
+
# First remove any existing annotations
|
26
|
+
content = File.read(file_path)
|
27
|
+
content = Annotation.remove(content) if Annotation.extract(content)
|
28
|
+
|
29
|
+
# Try AST-based insertion first
|
30
|
+
class_name = model_class.name.split('::').last
|
31
|
+
|
32
|
+
# Use Prism-based insertion
|
33
|
+
if FileInsertionHelper.insert_at_class_definition(file_path, class_name, annotation_text)
|
34
|
+
true
|
35
|
+
else
|
36
|
+
# Final fallback to old method
|
37
|
+
annotated_content = add_annotation(content, file_path)
|
38
|
+
if annotated_content == content
|
39
|
+
false
|
40
|
+
else
|
41
|
+
File.write(file_path, annotated_content)
|
42
|
+
true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def remove_annotations(file_path = nil)
|
48
|
+
file_path ||= model_file_path
|
49
|
+
return unless file_path && File.exist?(file_path)
|
50
|
+
|
51
|
+
content = File.read(file_path)
|
52
|
+
cleaned_content = Annotation.remove(content)
|
53
|
+
|
54
|
+
if cleaned_content == content
|
55
|
+
false
|
56
|
+
else
|
57
|
+
File.write(file_path, cleaned_content)
|
58
|
+
true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def generate_annotation
|
63
|
+
pipeline = AnnotationPipeline.new
|
64
|
+
results = pipeline.process(model_class)
|
65
|
+
|
66
|
+
annotation = Annotation.new
|
67
|
+
|
68
|
+
# Add schema content
|
69
|
+
annotation.add_lines(results[:schema].split("\n")) if results[:schema]
|
70
|
+
|
71
|
+
# Add sections
|
72
|
+
results[:sections].each do |section|
|
73
|
+
next unless section && section[:content]
|
74
|
+
|
75
|
+
annotation.add_line('')
|
76
|
+
# The provider can optionally provide a title
|
77
|
+
annotation.add_line(section[:title]) if section[:title]
|
78
|
+
annotation.add_lines(section[:content].split("\n"))
|
79
|
+
end
|
80
|
+
|
81
|
+
# Add notes
|
82
|
+
if results[:notes].any?
|
83
|
+
annotation.add_line('')
|
84
|
+
annotation.add_line('== Notes')
|
85
|
+
results[:notes].uniq.each do |note|
|
86
|
+
annotation.add_line("- #{note}")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
annotation.to_s
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.annotate_all(options = {})
|
94
|
+
models = ModelDetector.detect_models(options)
|
95
|
+
|
96
|
+
# Filter abstract classes based on options
|
97
|
+
if options[:include_abstract]
|
98
|
+
# Include all models
|
99
|
+
elsif options[:abstract_only]
|
100
|
+
models = models.select(&:abstract_class?)
|
101
|
+
else
|
102
|
+
# Default: exclude abstract classes
|
103
|
+
models = models.reject(&:abstract_class?)
|
104
|
+
end
|
105
|
+
|
106
|
+
results = { annotated: [], skipped: [], failed: [] }
|
107
|
+
|
108
|
+
models.each do |model|
|
109
|
+
# Ensure model is actually a class, not a hash or other object
|
110
|
+
unless model.is_a?(Class)
|
111
|
+
results[:failed] << { model: model.inspect, error: "Expected Class, got #{model.class}" }
|
112
|
+
next
|
113
|
+
end
|
114
|
+
|
115
|
+
# Skip models without tables or with missing tables (but not abstract classes)
|
116
|
+
unless model.abstract_class? || model.table_exists?
|
117
|
+
results[:skipped] << model.name
|
118
|
+
warn "Skipping #{model.name} - table does not exist" if options[:verbose]
|
119
|
+
next
|
120
|
+
end
|
121
|
+
|
122
|
+
manager = new(model)
|
123
|
+
|
124
|
+
# Determine file path based on options
|
125
|
+
file_path = if options[:models_path]
|
126
|
+
File.join(options[:models_path], "#{model.name.underscore}.rb")
|
127
|
+
else
|
128
|
+
nil # Use default model_file_path
|
129
|
+
end
|
130
|
+
|
131
|
+
# Allow external files when models_path is provided (for testing)
|
132
|
+
allow_external = options[:models_path].present?
|
133
|
+
|
134
|
+
if manager.annotate_file(file_path, allow_external_files: allow_external)
|
135
|
+
results[:annotated] << model.name
|
136
|
+
else
|
137
|
+
results[:skipped] << model.name
|
138
|
+
end
|
139
|
+
rescue ActiveRecord::StatementInvalid => e
|
140
|
+
# Handle database-related errors (missing tables, schemas, etc.)
|
141
|
+
results[:skipped] << model.name
|
142
|
+
warn "Skipping #{model.name} - database error: #{e.message}" if options[:verbose]
|
143
|
+
rescue StandardError => e
|
144
|
+
model_name = if model.is_a?(Class) && model.respond_to?(:name)
|
145
|
+
model.name
|
146
|
+
else
|
147
|
+
model.inspect
|
148
|
+
end
|
149
|
+
results[:failed] << { model: model_name, error: e.message }
|
150
|
+
end
|
151
|
+
|
152
|
+
results
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.remove_all(options = {})
|
156
|
+
models = ModelDetector.detect_models(options)
|
157
|
+
results = { removed: [], skipped: [], failed: [] }
|
158
|
+
|
159
|
+
models.each do |model|
|
160
|
+
manager = new(model)
|
161
|
+
if manager.remove_annotations
|
162
|
+
results[:removed] << model.name
|
163
|
+
else
|
164
|
+
results[:skipped] << model.name
|
165
|
+
end
|
166
|
+
rescue StandardError => e
|
167
|
+
results[:failed] << { model: model.name, error: e.message }
|
168
|
+
end
|
169
|
+
|
170
|
+
results
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
def add_annotation(content, _file_path = nil)
|
176
|
+
annotation_text = generate_annotation
|
177
|
+
|
178
|
+
# First check if annotation already exists and remove it
|
179
|
+
existing = Annotation.extract(content)
|
180
|
+
content = Annotation.remove(content) if existing
|
181
|
+
|
182
|
+
# Use the file insertion helper to insert after frozen_string_literal
|
183
|
+
FileInsertionHelper.insert_after_frozen_string_literal(content, annotation_text)
|
184
|
+
end
|
185
|
+
|
186
|
+
def model_file_path
|
187
|
+
# First try const_source_location as it's more reliable for finding model files
|
188
|
+
const_source_location = Object.const_source_location(model_class.name)
|
189
|
+
return const_source_location.first if const_source_location
|
190
|
+
|
191
|
+
# Fallback to instance method source location (though this often points to ActiveRecord)
|
192
|
+
model_class.instance_method(:initialize).source_location.first
|
193
|
+
rescue StandardError
|
194
|
+
# As a last resort, try to construct the path from Rails conventions
|
195
|
+
if defined?(Rails.root) && model_class.name
|
196
|
+
model_path = Rails.root.join('app', 'models', "#{model_class.name.underscore}.rb").to_s
|
197
|
+
model_path if File.exist?(model_path)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :rails_lens do
|
4
|
+
desc 'Annotate Rails models with schema information'
|
5
|
+
task :annotate, [:models] => :environment do |_t, args|
|
6
|
+
require 'rails_lens/schema/annotation_manager'
|
7
|
+
|
8
|
+
options = {
|
9
|
+
models: args[:models]&.split(',')
|
10
|
+
}
|
11
|
+
|
12
|
+
results = RailsLens::Schema::AnnotationManager.annotate_all(options)
|
13
|
+
|
14
|
+
puts "Annotated #{results[:annotated].length} models"
|
15
|
+
puts "Skipped #{results[:skipped].length} models" if results[:skipped].any?
|
16
|
+
if results[:failed].any?
|
17
|
+
puts "Failed to annotate #{results[:failed].length} models:"
|
18
|
+
results[:failed].each do |failure|
|
19
|
+
puts " - #{failure[:model]}: #{failure[:error]}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'Annotate all Rails files (models, routes, and mailers)'
|
25
|
+
task all: :environment do
|
26
|
+
# Annotate models
|
27
|
+
Rake::Task['rails_lens:annotate'].invoke
|
28
|
+
|
29
|
+
# Annotate routes
|
30
|
+
Rake::Task['rails_lens:routes:annotate'].invoke
|
31
|
+
|
32
|
+
# Annotate mailers
|
33
|
+
Rake::Task['rails_lens:mailers:annotate'].invoke
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :rails_lens do
|
4
|
+
desc 'Generate Entity Relationship Diagram (Mermaid format)'
|
5
|
+
task erd: :environment do
|
6
|
+
gem 'mermaid', '>= 0.0.5'
|
7
|
+
require 'mermaid'
|
8
|
+
|
9
|
+
puts 'Generating ERD...'
|
10
|
+
visualizer = RailsLens::ERD::Visualizer.new
|
11
|
+
filename = visualizer.generate
|
12
|
+
puts "ERD generated successfully: #{filename}"
|
13
|
+
puts ''
|
14
|
+
puts 'To view the ERD:'
|
15
|
+
puts '1. Install Mermaid CLI: npm install -g @mermaid-js/mermaid-cli'
|
16
|
+
puts "2. Generate image: mmdc -i #{filename} -o erd.png"
|
17
|
+
puts '3. Or view online: https://mermaid.live/'
|
18
|
+
rescue Gem::LoadError
|
19
|
+
puts 'Error: Mermaid gem (>= 0.0.5) is required for ERD generation.'
|
20
|
+
puts "Add to your Gemfile: gem 'mermaid', '>= 0.0.5'"
|
21
|
+
puts 'Then run: bundle install'
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :rails_lens do
|
4
|
+
namespace :mailers do
|
5
|
+
desc 'Annotate mailer files with mailer information'
|
6
|
+
task annotate: :environment do
|
7
|
+
require 'rails_lens/mailer/annotator'
|
8
|
+
|
9
|
+
annotator = RailsLens::Mailer::Annotator.new
|
10
|
+
changed_files = annotator.annotate_all
|
11
|
+
|
12
|
+
puts "Annotated #{changed_files.length} mailer files with mailer information"
|
13
|
+
changed_files.each { |file| puts " - #{file}" }
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Remove mailer annotations from mailer files'
|
17
|
+
task remove: :environment do
|
18
|
+
require 'rails_lens/mailer/annotator'
|
19
|
+
|
20
|
+
annotator = RailsLens::Mailer::Annotator.new
|
21
|
+
changed_files = annotator.remove_all
|
22
|
+
|
23
|
+
puts "Removed mailer annotations from #{changed_files.length} mailer files"
|
24
|
+
changed_files.each { |file| puts " - #{file}" }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :rails_lens do
|
4
|
+
namespace :routes do
|
5
|
+
desc 'Annotate controller files with route information'
|
6
|
+
task annotate: :environment do
|
7
|
+
require 'rails_lens/route/annotator'
|
8
|
+
|
9
|
+
annotator = RailsLens::Route::Annotator.new
|
10
|
+
changed_files = annotator.annotate_all
|
11
|
+
|
12
|
+
puts "Annotated #{changed_files.length} controller files with route information"
|
13
|
+
changed_files.each { |file| puts " - #{file}" }
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Remove route annotations from controller files'
|
17
|
+
task remove: :environment do
|
18
|
+
require 'rails_lens/route/annotator'
|
19
|
+
|
20
|
+
annotator = RailsLens::Route::Annotator.new
|
21
|
+
changed_files = annotator.remove_all
|
22
|
+
|
23
|
+
puts "Removed route annotations from #{changed_files.length} controller files"
|
24
|
+
changed_files.each { |file| puts " - #{file}" }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :rails_lens do
|
4
|
+
namespace :schema do
|
5
|
+
desc 'Annotate models with database schema information'
|
6
|
+
task annotate: :environment do
|
7
|
+
require 'rails_lens'
|
8
|
+
|
9
|
+
options = {}
|
10
|
+
options[:include_abstract] = true if ENV['INCLUDE_ABSTRACT'] == 'true'
|
11
|
+
|
12
|
+
puts 'Annotating models with schema information...'
|
13
|
+
results = RailsLens.annotate_models(options)
|
14
|
+
|
15
|
+
if results[:annotated].any?
|
16
|
+
puts "Annotated #{results[:annotated].length} models"
|
17
|
+
puts '(including abstract classes)' if options[:include_abstract]
|
18
|
+
end
|
19
|
+
|
20
|
+
puts "\nSkipped #{results[:skipped].length} models (no changes needed)" if results[:skipped].any?
|
21
|
+
|
22
|
+
if results[:failed].any?
|
23
|
+
puts "\nFailed to annotate #{results[:failed].length} models:"
|
24
|
+
results[:failed].each do |failure|
|
25
|
+
puts " ✗ #{failure[:model]}: #{failure[:error]}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
desc 'Remove schema annotations from models'
|
31
|
+
task remove: :environment do
|
32
|
+
require 'rails_lens'
|
33
|
+
|
34
|
+
puts 'Removing schema annotations...'
|
35
|
+
results = RailsLens.remove_annotations
|
36
|
+
|
37
|
+
if results[:removed].any?
|
38
|
+
puts "Removed annotations from #{results[:removed].length} models:"
|
39
|
+
results[:removed].each { |model| puts " ✓ #{model}" }
|
40
|
+
end
|
41
|
+
|
42
|
+
puts "\nSkipped #{results[:skipped].length} models (no annotations found)" if results[:skipped].any?
|
43
|
+
|
44
|
+
if results[:failed].any?
|
45
|
+
puts "\nFailed to process #{results[:failed].length} models:"
|
46
|
+
results[:failed].each do |failure|
|
47
|
+
puts " ✗ #{failure[:model]}: #{failure[:error]}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
desc 'Analyze models and show notes'
|
53
|
+
task analyze: :environment do
|
54
|
+
require 'rails_lens'
|
55
|
+
|
56
|
+
puts 'Analyzing models...'
|
57
|
+
|
58
|
+
pipeline = RailsLens::AnnotationPipeline.new
|
59
|
+
models = RailsLens::ModelDetector.detect_models
|
60
|
+
|
61
|
+
models.each do |model|
|
62
|
+
next if model.abstract_class? || !model.table_exists?
|
63
|
+
|
64
|
+
results = pipeline.process(model)
|
65
|
+
notes = results[:notes]
|
66
|
+
|
67
|
+
if notes.any?
|
68
|
+
puts "\n#{model.name}:"
|
69
|
+
notes.uniq.each { |note| puts " - #{note}" }
|
70
|
+
end
|
71
|
+
rescue StandardError => e
|
72
|
+
puts "\nError analyzing #{model.name}: #{e.message}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
desc 'List detected extensions'
|
77
|
+
task extensions: :environment do
|
78
|
+
require 'rails_lens'
|
79
|
+
|
80
|
+
puts 'Detected extensions:'
|
81
|
+
|
82
|
+
extensions = RailsLens::ExtensionLoader.load_extensions
|
83
|
+
if extensions.any?
|
84
|
+
extensions.each do |ext|
|
85
|
+
status = ext.detect? ? '✓ Active' : '○ Inactive'
|
86
|
+
version = begin
|
87
|
+
ext.interface_version
|
88
|
+
rescue StandardError
|
89
|
+
'Unknown'
|
90
|
+
end
|
91
|
+
puts " #{status} #{ext.gem_name} (interface v#{version})"
|
92
|
+
end
|
93
|
+
else
|
94
|
+
puts ' No extensions detected'
|
95
|
+
end
|
96
|
+
|
97
|
+
config = RailsLens.config.extensions
|
98
|
+
puts "\nExtension configuration:"
|
99
|
+
puts " Enabled: #{config[:enabled]}"
|
100
|
+
puts " Autoload: #{config[:autoload]}"
|
101
|
+
puts " Interface version: #{config[:interface_version]}"
|
102
|
+
|
103
|
+
puts " Ignored gems: #{config[:ignore].join(', ')}" if config[:ignore].any?
|
104
|
+
|
105
|
+
puts " Custom paths: #{config[:custom_paths].join(', ')}" if config[:custom_paths].any?
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|