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,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
module Analyzers
|
5
|
+
class PerformanceAnalyzer < Base
|
6
|
+
def analyze
|
7
|
+
notes = []
|
8
|
+
notes.concat(analyze_large_text_columns)
|
9
|
+
notes.concat(analyze_uuid_indexes)
|
10
|
+
notes.concat(analyze_query_performance)
|
11
|
+
notes
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def analyze_large_text_columns
|
17
|
+
notes = []
|
18
|
+
|
19
|
+
text_columns.each do |column|
|
20
|
+
if frequently_queried_column?(column)
|
21
|
+
notes << "Large text column '#{column.name}' is frequently queried - consider separate storage"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
notes
|
26
|
+
end
|
27
|
+
|
28
|
+
def analyze_uuid_indexes
|
29
|
+
notes = []
|
30
|
+
|
31
|
+
uuid_columns.each do |column|
|
32
|
+
next if column.name == 'id' # Primary keys are already indexed
|
33
|
+
|
34
|
+
if should_be_indexed?(column) && !indexed?(column)
|
35
|
+
notes << "UUID column '#{column.name}' should be indexed for better query performance"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
notes
|
40
|
+
end
|
41
|
+
|
42
|
+
def analyze_query_performance
|
43
|
+
notes = []
|
44
|
+
|
45
|
+
# Check for columns that are commonly used in WHERE clauses
|
46
|
+
commonly_queried_columns.each do |column|
|
47
|
+
next if indexed?(column)
|
48
|
+
|
49
|
+
notes << "Column '#{column.name}' is commonly used in queries - consider adding an index"
|
50
|
+
end
|
51
|
+
|
52
|
+
# Check for missing indexes on scoped columns
|
53
|
+
scoped_columns.each do |column|
|
54
|
+
next if indexed?(column)
|
55
|
+
|
56
|
+
notes << "Scope column '#{column.name}' should be indexed"
|
57
|
+
end
|
58
|
+
|
59
|
+
notes
|
60
|
+
end
|
61
|
+
|
62
|
+
def text_columns
|
63
|
+
model_class.columns.select { |c| c.type == :text }
|
64
|
+
end
|
65
|
+
|
66
|
+
def uuid_columns
|
67
|
+
model_class.columns.select { |c| c.type == :uuid || (c.type == :string && c.name.match?(/uuid|guid/i)) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def frequently_queried_column?(column)
|
71
|
+
# Heuristic: columns with certain names are likely to be queried frequently
|
72
|
+
column.name.match?(/title|name|slug|description|summary|content|body/i)
|
73
|
+
end
|
74
|
+
|
75
|
+
def should_be_indexed?(column)
|
76
|
+
# UUID columns used as foreign keys or identifiers should be indexed
|
77
|
+
column.name.end_with?('_id', '_uuid', '_guid') ||
|
78
|
+
column.name.match?(/identifier|reference|token/i)
|
79
|
+
end
|
80
|
+
|
81
|
+
def commonly_queried_columns
|
82
|
+
model_class.columns.select do |column|
|
83
|
+
column.name.match?(/email|username|slug|token|code|status|state|type/i) ||
|
84
|
+
column.name.end_with?('_type', '_kind', '_category')
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def scoped_columns
|
89
|
+
model_class.columns.select do |column|
|
90
|
+
column.name.match?(/scope|tenant|company|organization|account|workspace/i) &&
|
91
|
+
column.name.end_with?('_id')
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def indexed?(column)
|
96
|
+
connection.indexes(table_name).any? do |index|
|
97
|
+
index.columns.include?(column.name)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def connection
|
102
|
+
model_class.connection
|
103
|
+
end
|
104
|
+
|
105
|
+
def table_name
|
106
|
+
model_class.table_name
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
# Manages the pipeline of content providers for generating annotations
|
5
|
+
class AnnotationPipeline
|
6
|
+
attr_reader :providers
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@providers = []
|
10
|
+
register_default_providers
|
11
|
+
end
|
12
|
+
|
13
|
+
def register(provider)
|
14
|
+
@providers << provider
|
15
|
+
end
|
16
|
+
|
17
|
+
def unregister(provider_class)
|
18
|
+
@providers.reject! { |p| p.is_a?(provider_class) }
|
19
|
+
end
|
20
|
+
|
21
|
+
delegate :clear, to: :@providers
|
22
|
+
|
23
|
+
def process(model_class)
|
24
|
+
results = {
|
25
|
+
schema: nil,
|
26
|
+
sections: [],
|
27
|
+
notes: []
|
28
|
+
}
|
29
|
+
|
30
|
+
@providers.each do |provider|
|
31
|
+
next unless provider.applicable?(model_class)
|
32
|
+
|
33
|
+
begin
|
34
|
+
result = provider.process(model_class)
|
35
|
+
|
36
|
+
case provider.type
|
37
|
+
when :schema
|
38
|
+
results[:schema] = result
|
39
|
+
when :section
|
40
|
+
results[:sections] << result if result
|
41
|
+
when :notes
|
42
|
+
results[:notes].concat(Array(result))
|
43
|
+
end
|
44
|
+
rescue ActiveRecord::StatementInvalid => e
|
45
|
+
warn "Provider #{provider.class} database error for #{model_class}: #{e.message}"
|
46
|
+
rescue ActiveRecord::ConnectionNotDefined => e
|
47
|
+
warn "Provider #{provider.class} connection error for #{model_class}: #{e.message}"
|
48
|
+
rescue NameError, NoMethodError => e
|
49
|
+
warn "Provider #{provider.class} method error for #{model_class}: #{e.message}"
|
50
|
+
rescue RailsLens::Error => e
|
51
|
+
warn "Provider #{provider.class} rails_lens error for #{model_class}: #{e.message}"
|
52
|
+
rescue StandardError => e
|
53
|
+
warn "Provider #{provider.class} unexpected error for #{model_class}: #{e.message}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
results
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def register_default_providers
|
63
|
+
# Schema provider (primary content)
|
64
|
+
register(Providers::SchemaProvider.new)
|
65
|
+
|
66
|
+
# Section providers (additional structured content)
|
67
|
+
register(Providers::ExtensionsProvider.new) if RailsLens.config.extensions[:enabled]
|
68
|
+
register(Providers::InheritanceProvider.new)
|
69
|
+
register(Providers::EnumsProvider.new)
|
70
|
+
register(Providers::DelegatedTypesProvider.new)
|
71
|
+
register(Providers::CompositeKeysProvider.new)
|
72
|
+
register(Providers::DatabaseConstraintsProvider.new)
|
73
|
+
register(Providers::GeneratedColumnsProvider.new)
|
74
|
+
|
75
|
+
# Notes providers (analysis and recommendations)
|
76
|
+
return unless RailsLens.config.schema[:include_notes]
|
77
|
+
|
78
|
+
register(Providers::IndexNotesProvider.new)
|
79
|
+
register(Providers::ForeignKeyNotesProvider.new)
|
80
|
+
register(Providers::AssociationNotesProvider.new)
|
81
|
+
register(Providers::ColumnNotesProvider.new)
|
82
|
+
register(Providers::PerformanceNotesProvider.new)
|
83
|
+
register(Providers::BestPracticesNotesProvider.new)
|
84
|
+
register(Providers::ExtensionNotesProvider.new) if RailsLens.config.extensions[:enabled]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require_relative 'commands'
|
5
|
+
require_relative 'cli_error_handler'
|
6
|
+
|
7
|
+
module RailsLens
|
8
|
+
class CLI < Thor
|
9
|
+
include CLIErrorHandler
|
10
|
+
|
11
|
+
# Thor configuration: exit with proper status codes on failure (modern behavior)
|
12
|
+
def self.exit_on_failure?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
class_option :config, type: :string, default: '.rails-lens.yml', desc: 'Configuration file path'
|
17
|
+
class_option :dry_run, type: :boolean, desc: 'Show what would be done without making changes'
|
18
|
+
class_option :verbose, type: :boolean, desc: 'Verbose output'
|
19
|
+
class_option :debug, type: :boolean, desc: 'Debug output with full backtraces'
|
20
|
+
|
21
|
+
desc 'annotate', 'Annotate Rails models with schema information'
|
22
|
+
option :models, type: :array, desc: 'Specific models to annotate'
|
23
|
+
option :include_abstract, type: :boolean, desc: 'Include abstract classes'
|
24
|
+
option :position, type: :string, enum: %w[before after top bottom], desc: 'Annotation position'
|
25
|
+
option :routes, type: :boolean, desc: 'Annotate controller routes'
|
26
|
+
option :mailers, type: :boolean, desc: 'Annotate mailer methods'
|
27
|
+
option :all, type: :boolean, desc: 'Annotate models, routes, and mailers'
|
28
|
+
def annotate
|
29
|
+
with_error_handling do
|
30
|
+
setup_environment
|
31
|
+
|
32
|
+
results = {}
|
33
|
+
commands = Commands.new(self)
|
34
|
+
|
35
|
+
# Annotate models (default behavior or when --all is specified)
|
36
|
+
results[:models] = commands.annotate_models(options) if should_annotate_models?
|
37
|
+
|
38
|
+
# Annotate routes
|
39
|
+
results[:routes] = commands.annotate_routes(options) if should_annotate_routes?
|
40
|
+
|
41
|
+
# Annotate mailers
|
42
|
+
results[:mailers] = commands.annotate_mailers(options) if should_annotate_mailers?
|
43
|
+
|
44
|
+
results
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
desc 'remove', 'Remove annotations from Rails files'
|
49
|
+
option :routes, type: :boolean, desc: 'Remove controller route annotations'
|
50
|
+
option :mailers, type: :boolean, desc: 'Remove mailer annotations'
|
51
|
+
option :all, type: :boolean, desc: 'Remove all annotations'
|
52
|
+
def remove
|
53
|
+
with_error_handling do
|
54
|
+
setup_environment
|
55
|
+
|
56
|
+
results = {}
|
57
|
+
commands = Commands.new(self)
|
58
|
+
|
59
|
+
# Remove model annotations (default behavior or when --all is specified)
|
60
|
+
results[:models] = commands.remove_models(options) if should_remove_models?
|
61
|
+
|
62
|
+
# Remove route annotations
|
63
|
+
results[:routes] = commands.remove_routes(options) if should_remove_routes?
|
64
|
+
|
65
|
+
# Remove mailer annotations
|
66
|
+
results[:mailers] = commands.remove_mailers(options) if should_remove_mailers?
|
67
|
+
|
68
|
+
results
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
desc 'erd', 'Generate Entity Relationship Diagram (Mermaid format)'
|
73
|
+
option :output, type: :string, desc: 'Output directory'
|
74
|
+
option :verbose, type: :boolean, desc: 'Verbose output'
|
75
|
+
option :group_by_database, type: :boolean, desc: 'Group models by database connection instead of domain'
|
76
|
+
def erd
|
77
|
+
with_error_handling do
|
78
|
+
setup_environment
|
79
|
+
|
80
|
+
# Transform CLI options to visualizer options
|
81
|
+
visualizer_options = options.dup
|
82
|
+
visualizer_options[:output_dir] = options[:output] || 'output'
|
83
|
+
|
84
|
+
commands = Commands.new(self)
|
85
|
+
commands.generate_erd(visualizer_options)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
desc 'version', 'Show Rails Lens version'
|
90
|
+
def version
|
91
|
+
say "Rails Lens #{RailsLens::VERSION}"
|
92
|
+
end
|
93
|
+
|
94
|
+
desc 'lint', 'Lint Rails Lens configuration and model annotations'
|
95
|
+
option :domains, type: :array, desc: 'Specific domains to lint (models, routes, mailers)'
|
96
|
+
def lint
|
97
|
+
with_error_handling do
|
98
|
+
setup_environment
|
99
|
+
|
100
|
+
commands = Commands.new(self)
|
101
|
+
commands.lint(options)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
desc 'check', 'Check Rails Lens configuration validity'
|
106
|
+
def check
|
107
|
+
with_error_handling do
|
108
|
+
setup_environment
|
109
|
+
|
110
|
+
commands = Commands.new(self)
|
111
|
+
commands.check(options)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
desc 'config SUBCOMMAND', 'Manage Rails Lens configuration'
|
116
|
+
option :key, type: :string, desc: 'Configuration key'
|
117
|
+
option :value, type: :string, desc: 'Configuration value'
|
118
|
+
def config(subcommand = 'show')
|
119
|
+
with_error_handling do
|
120
|
+
setup_environment
|
121
|
+
|
122
|
+
commands = Commands.new(self)
|
123
|
+
commands.config(subcommand, options)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def setup_environment
|
130
|
+
RakeBootstrapper.call
|
131
|
+
load_configuration(options[:config])
|
132
|
+
configure_error_reporting
|
133
|
+
end
|
134
|
+
|
135
|
+
def load_configuration(config_file)
|
136
|
+
if File.exist?(config_file)
|
137
|
+
RailsLens.load_config_file(config_file)
|
138
|
+
say "Loaded configuration from #{config_file}", :green if options[:verbose]
|
139
|
+
elsif config_file != '.rails-lens.yml'
|
140
|
+
raise ConfigurationError, "Configuration file not found: #{config_file}"
|
141
|
+
elsif options[:verbose]
|
142
|
+
say "Using default configuration (#{config_file} not found)", :yellow
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def configure_error_reporting
|
147
|
+
RailsLens.config.verbose = options[:verbose]
|
148
|
+
RailsLens.config.debug = options[:debug]
|
149
|
+
end
|
150
|
+
|
151
|
+
# Helper methods to determine what to annotate/remove
|
152
|
+
def should_annotate_models?
|
153
|
+
(!options[:routes] && !options[:mailers]) || options[:all]
|
154
|
+
end
|
155
|
+
|
156
|
+
def should_annotate_routes?
|
157
|
+
options[:routes] || options[:all]
|
158
|
+
end
|
159
|
+
|
160
|
+
def should_annotate_mailers?
|
161
|
+
options[:mailers] || options[:all]
|
162
|
+
end
|
163
|
+
|
164
|
+
def should_remove_models?
|
165
|
+
(!options[:routes] && !options[:mailers]) || options[:all]
|
166
|
+
end
|
167
|
+
|
168
|
+
def should_remove_routes?
|
169
|
+
options[:routes] || options[:all]
|
170
|
+
end
|
171
|
+
|
172
|
+
def should_remove_mailers?
|
173
|
+
options[:mailers] || options[:all]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
# Provides consistent error handling for CLI commands
|
5
|
+
module CLIErrorHandler
|
6
|
+
def with_error_handling
|
7
|
+
yield
|
8
|
+
rescue Interrupt
|
9
|
+
say "\nOperation cancelled by user", :yellow
|
10
|
+
exit 1
|
11
|
+
rescue ConfigurationError => e
|
12
|
+
handle_configuration_error(e)
|
13
|
+
rescue ModelDetectionError => e
|
14
|
+
handle_model_error(e)
|
15
|
+
rescue DatabaseError => e
|
16
|
+
handle_database_error(e)
|
17
|
+
rescue AnnotationError => e
|
18
|
+
handle_annotation_error(e)
|
19
|
+
rescue ExtensionError => e
|
20
|
+
handle_extension_error(e)
|
21
|
+
rescue StandardError => e
|
22
|
+
handle_unexpected_error(e)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def handle_configuration_error(error)
|
28
|
+
say "Configuration Error: #{error.message}", :red
|
29
|
+
say 'Please check your .rails-lens.yml file', :yellow
|
30
|
+
exit 1
|
31
|
+
end
|
32
|
+
|
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
|
40
|
+
end
|
41
|
+
|
42
|
+
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
|
52
|
+
end
|
53
|
+
|
54
|
+
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
|
60
|
+
exit 1
|
61
|
+
end
|
62
|
+
|
63
|
+
def handle_extension_error(error)
|
64
|
+
say "Extension Error: #{error.message}", :red
|
65
|
+
return unless options[:verbose]
|
66
|
+
|
67
|
+
say 'An extension failed to load or execute', :yellow
|
68
|
+
say 'You can disable extensions in .rails-lens.yml', :yellow
|
69
|
+
|
70
|
+
# Don't exit - extensions are optional
|
71
|
+
end
|
72
|
+
|
73
|
+
def handle_unexpected_error(error)
|
74
|
+
say "Unexpected Error: #{error.class.name}", :red
|
75
|
+
say error.message, :red
|
76
|
+
|
77
|
+
if options[:verbose] || options[:debug]
|
78
|
+
say "\nBacktrace:", :yellow
|
79
|
+
say error.backtrace.first(10).join("\n"), :yellow
|
80
|
+
end
|
81
|
+
|
82
|
+
say "\nPlease report this issue at: https://github.com/your-org/rails_lens/issues", :cyan
|
83
|
+
exit 1
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
# Handles the actual execution of CLI commands
|
5
|
+
class Commands
|
6
|
+
attr_reader :output
|
7
|
+
|
8
|
+
def initialize(output = $stdout)
|
9
|
+
@output = output
|
10
|
+
end
|
11
|
+
|
12
|
+
def annotate_models(options = {})
|
13
|
+
results = Schema::AnnotationManager.annotate_all(options)
|
14
|
+
|
15
|
+
output.say "Annotated #{results[:annotated].length} models", :green
|
16
|
+
output.say "Skipped #{results[:skipped].length} models", :yellow if results[:skipped].any?
|
17
|
+
|
18
|
+
if results[:failed].any?
|
19
|
+
output.say "Failed to annotate #{results[:failed].length} models:", :red
|
20
|
+
results[:failed].each do |failure|
|
21
|
+
output.say " - #{failure[:model]}: #{failure[:error]}", :red
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
results
|
26
|
+
end
|
27
|
+
|
28
|
+
def annotate_routes(options = {})
|
29
|
+
annotator = Route::Annotator.new(dry_run: options[:dry_run])
|
30
|
+
changed_files = annotator.annotate_all
|
31
|
+
|
32
|
+
output.say "Annotated #{changed_files.length} controller files with routes", :green
|
33
|
+
changed_files.each { |file| output.say " - #{file}", :blue } if options[:verbose] && changed_files.any?
|
34
|
+
|
35
|
+
{ changed_files: changed_files }
|
36
|
+
end
|
37
|
+
|
38
|
+
def annotate_mailers(options = {})
|
39
|
+
annotator = Mailer::Annotator.new(dry_run: options[:dry_run])
|
40
|
+
changed_files = annotator.annotate_all
|
41
|
+
|
42
|
+
output.say "Annotated #{changed_files.length} mailer files", :green
|
43
|
+
changed_files.each { |file| output.say " - #{file}", :blue } if options[:verbose] && changed_files.any?
|
44
|
+
|
45
|
+
{ changed_files: changed_files }
|
46
|
+
end
|
47
|
+
|
48
|
+
def remove_models(options = {})
|
49
|
+
results = Schema::AnnotationManager.remove_all(options)
|
50
|
+
output.say "Removed annotations from #{results[:removed].length} models", :green
|
51
|
+
results
|
52
|
+
end
|
53
|
+
|
54
|
+
def remove_routes(options = {})
|
55
|
+
annotator = Route::Annotator.new(dry_run: options[:dry_run])
|
56
|
+
changed_files = annotator.remove_all
|
57
|
+
output.say "Removed route annotations from #{changed_files.length} controller files", :green
|
58
|
+
{ changed_files: changed_files }
|
59
|
+
end
|
60
|
+
|
61
|
+
def remove_mailers(options = {})
|
62
|
+
annotator = Mailer::Annotator.new(dry_run: options[:dry_run])
|
63
|
+
changed_files = annotator.remove_all
|
64
|
+
output.say "Removed mailer annotations from #{changed_files.length} mailer files", :green
|
65
|
+
{ changed_files: changed_files }
|
66
|
+
end
|
67
|
+
|
68
|
+
def generate_erd(options = {})
|
69
|
+
visualizer = ERD::Visualizer.new(options: options)
|
70
|
+
filename = visualizer.generate
|
71
|
+
output.say "Entity Relationship Diagram generated at #{filename}", :green
|
72
|
+
filename
|
73
|
+
end
|
74
|
+
|
75
|
+
def lint(options = {})
|
76
|
+
output.say 'Linting Rails Lens configuration and annotations...', :blue
|
77
|
+
|
78
|
+
issues = []
|
79
|
+
warnings = []
|
80
|
+
domains = options[:domains] || %w[models routes mailers]
|
81
|
+
|
82
|
+
domains.each do |domain|
|
83
|
+
case domain
|
84
|
+
when 'models'
|
85
|
+
# Check for inconsistent model annotations
|
86
|
+
output.say ' Checking models domain...', :blue if options[:verbose]
|
87
|
+
when 'routes'
|
88
|
+
# Check for route annotation consistency
|
89
|
+
output.say ' Checking routes domain...', :blue if options[:verbose]
|
90
|
+
when 'mailers'
|
91
|
+
# Check for mailer annotation consistency
|
92
|
+
output.say ' Checking mailers domain...', :blue if options[:verbose]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
if issues.empty? && warnings.empty?
|
97
|
+
output.say 'No linting issues found', :green
|
98
|
+
else
|
99
|
+
output.say "Found #{issues.length} issues and #{warnings.length} warnings", :yellow
|
100
|
+
end
|
101
|
+
|
102
|
+
{ issues: issues, warnings: warnings }
|
103
|
+
end
|
104
|
+
|
105
|
+
def check(_options = {})
|
106
|
+
output.say 'Checking Rails Lens configuration validity...', :blue
|
107
|
+
|
108
|
+
# Check if Rails is properly loaded
|
109
|
+
unless defined?(Rails)
|
110
|
+
output.say 'Rails environment not detected', :red
|
111
|
+
return { valid: false, errors: ['Rails environment not detected'] }
|
112
|
+
end
|
113
|
+
|
114
|
+
# Check database connections
|
115
|
+
errors = []
|
116
|
+
begin
|
117
|
+
ApplicationRecord.connection.execute('SELECT 1')
|
118
|
+
output.say 'Primary database connection: OK', :green
|
119
|
+
rescue StandardError => e
|
120
|
+
errors << "Primary database connection failed: #{e.message}"
|
121
|
+
output.say 'Primary database connection: FAILED', :red
|
122
|
+
end
|
123
|
+
|
124
|
+
if errors.empty?
|
125
|
+
output.say 'Configuration is valid', :green
|
126
|
+
{ valid: true, errors: [] }
|
127
|
+
else
|
128
|
+
output.say 'Configuration has errors', :red
|
129
|
+
{ valid: false, errors: errors }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def config(subcommand, options = {})
|
134
|
+
case subcommand
|
135
|
+
when 'show'
|
136
|
+
output.say 'Rails Lens Configuration:', :blue
|
137
|
+
output.say " Config file: #{RailsLens.config_file || 'default'}"
|
138
|
+
output.say " Verbose: #{RailsLens.config.verbose || false}"
|
139
|
+
output.say " Debug: #{RailsLens.config.debug || false}"
|
140
|
+
|
141
|
+
if RailsLens.config.respond_to?(:position)
|
142
|
+
output.say " Default position: #{RailsLens.config.position || 'before'}"
|
143
|
+
end
|
144
|
+
|
145
|
+
when 'set'
|
146
|
+
if options[:key] && options[:value]
|
147
|
+
output.say "Setting #{options[:key]} = #{options[:value]}", :green
|
148
|
+
# NOTE: This would require implementing config persistence
|
149
|
+
output.say 'Note: Configuration changes are not persisted yet', :yellow
|
150
|
+
else
|
151
|
+
output.say 'Usage: config set --key KEY --value VALUE', :red
|
152
|
+
end
|
153
|
+
|
154
|
+
when 'reset'
|
155
|
+
output.say 'Resetting configuration to defaults', :yellow
|
156
|
+
# NOTE: This would reset to defaults
|
157
|
+
|
158
|
+
else
|
159
|
+
output.say "Unknown config subcommand: #{subcommand}", :red
|
160
|
+
output.say 'Available: show, set, reset'
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|