railsforge 1.0.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +528 -0
- data/bin/railsforge +8 -0
- data/lib/railsforge/analyzers/base_analyzer.rb +41 -0
- data/lib/railsforge/analyzers/controller_analyzer.rb +83 -0
- data/lib/railsforge/analyzers/database_analyzer.rb +55 -0
- data/lib/railsforge/analyzers/metrics_analyzer.rb +55 -0
- data/lib/railsforge/analyzers/model_analyzer.rb +74 -0
- data/lib/railsforge/analyzers/performance_analyzer.rb +161 -0
- data/lib/railsforge/analyzers/refactor_analyzer.rb +118 -0
- data/lib/railsforge/analyzers/security_analyzer.rb +169 -0
- data/lib/railsforge/analyzers/spec_analyzer.rb +58 -0
- data/lib/railsforge/api_generator.rb +397 -0
- data/lib/railsforge/audit.rb +289 -0
- data/lib/railsforge/cli.rb +671 -0
- data/lib/railsforge/config.rb +181 -0
- data/lib/railsforge/database_analyzer.rb +300 -0
- data/lib/railsforge/doctor.rb +250 -0
- data/lib/railsforge/feature_generator.rb +560 -0
- data/lib/railsforge/generator.rb +313 -0
- data/lib/railsforge/generators/base_generator.rb +70 -0
- data/lib/railsforge/generators/demo_generator.rb +307 -0
- data/lib/railsforge/generators/devops_generator.rb +287 -0
- data/lib/railsforge/generators/monitoring_generator.rb +134 -0
- data/lib/railsforge/generators/service_generator.rb +122 -0
- data/lib/railsforge/generators/stimulus_controller_generator.rb +129 -0
- data/lib/railsforge/generators/test_generator.rb +289 -0
- data/lib/railsforge/generators/view_component_generator.rb +169 -0
- data/lib/railsforge/graph.rb +270 -0
- data/lib/railsforge/loader.rb +56 -0
- data/lib/railsforge/mailer_generator.rb +191 -0
- data/lib/railsforge/plugins/plugin_loader.rb +60 -0
- data/lib/railsforge/plugins.rb +30 -0
- data/lib/railsforge/profiles/admin_app.yml +49 -0
- data/lib/railsforge/profiles/api_only.yml +47 -0
- data/lib/railsforge/profiles/blog.yml +47 -0
- data/lib/railsforge/profiles/standard.yml +44 -0
- data/lib/railsforge/profiles.rb +99 -0
- data/lib/railsforge/refactor_analyzer.rb +401 -0
- data/lib/railsforge/refactor_controller.rb +277 -0
- data/lib/railsforge/refactors/refactor_controller.rb +117 -0
- data/lib/railsforge/template_loader.rb +105 -0
- data/lib/railsforge/templates/v1/form/spec_template.rb +18 -0
- data/lib/railsforge/templates/v1/form/template.rb +28 -0
- data/lib/railsforge/templates/v1/job/spec_template.rb +17 -0
- data/lib/railsforge/templates/v1/job/template.rb +13 -0
- data/lib/railsforge/templates/v1/policy/spec_template.rb +41 -0
- data/lib/railsforge/templates/v1/policy/template.rb +57 -0
- data/lib/railsforge/templates/v1/presenter/spec_template.rb +12 -0
- data/lib/railsforge/templates/v1/presenter/template.rb +13 -0
- data/lib/railsforge/templates/v1/query/spec_template.rb +12 -0
- data/lib/railsforge/templates/v1/query/template.rb +16 -0
- data/lib/railsforge/templates/v1/serializer/spec_template.rb +13 -0
- data/lib/railsforge/templates/v1/serializer/template.rb +11 -0
- data/lib/railsforge/templates/v1/service/spec_template.rb +12 -0
- data/lib/railsforge/templates/v1/service/template.rb +25 -0
- data/lib/railsforge/templates/v1/stimulus_controller/template.rb +35 -0
- data/lib/railsforge/templates/v1/view_component/template.rb +24 -0
- data/lib/railsforge/templates/v2/job/template.rb +49 -0
- data/lib/railsforge/templates/v2/query/template.rb +66 -0
- data/lib/railsforge/templates/v2/service/spec_template.rb +33 -0
- data/lib/railsforge/templates/v2/service/template.rb +71 -0
- data/lib/railsforge/templates/v3/job/template.rb +72 -0
- data/lib/railsforge/templates/v3/query/spec_template.rb +54 -0
- data/lib/railsforge/templates/v3/query/template.rb +115 -0
- data/lib/railsforge/templates/v3/service/spec_template.rb +51 -0
- data/lib/railsforge/templates/v3/service/template.rb +84 -0
- data/lib/railsforge/version.rb +5 -0
- data/lib/railsforge/wizard.rb +265 -0
- data/lib/railsforge/wizard_tui.rb +286 -0
- data/lib/railsforge.rb +13 -0
- metadata +216 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Blog Profile Configuration
|
|
2
|
+
# Use with: railsforge new my_blog --profile=blog
|
|
3
|
+
|
|
4
|
+
name: blog
|
|
5
|
+
description: A standard blog application with articles, comments, and user authentication
|
|
6
|
+
|
|
7
|
+
# Default folders to create
|
|
8
|
+
folders:
|
|
9
|
+
- app/controllers
|
|
10
|
+
- app/models
|
|
11
|
+
- app/views
|
|
12
|
+
- app/helpers
|
|
13
|
+
- app/mailers
|
|
14
|
+
- app/jobs
|
|
15
|
+
- app/services
|
|
16
|
+
- app/queries
|
|
17
|
+
- app/presenters
|
|
18
|
+
- app/policies
|
|
19
|
+
- app/serializers
|
|
20
|
+
- app/forms
|
|
21
|
+
- config/initializers
|
|
22
|
+
- db/migrate
|
|
23
|
+
- spec/models
|
|
24
|
+
- spec/controllers
|
|
25
|
+
- spec/views
|
|
26
|
+
- spec/requests
|
|
27
|
+
- spec/services
|
|
28
|
+
- spec/policies
|
|
29
|
+
- spec/serializers
|
|
30
|
+
|
|
31
|
+
# Default generators to run after app creation
|
|
32
|
+
generators:
|
|
33
|
+
- model
|
|
34
|
+
- scaffold
|
|
35
|
+
- resource
|
|
36
|
+
|
|
37
|
+
# Features to enable by default
|
|
38
|
+
features:
|
|
39
|
+
- authentication
|
|
40
|
+
- authorization
|
|
41
|
+
- mailers
|
|
42
|
+
|
|
43
|
+
# Template variables
|
|
44
|
+
defaults:
|
|
45
|
+
css_framework: tailwind
|
|
46
|
+
testing: rspec
|
|
47
|
+
database: postgresql
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Standard Profile Configuration
|
|
2
|
+
# Use with: railsforge new my_app --profile=standard (default)
|
|
3
|
+
|
|
4
|
+
name: standard
|
|
5
|
+
description: A standard Rails application with common defaults
|
|
6
|
+
|
|
7
|
+
# Default folders to create
|
|
8
|
+
folders:
|
|
9
|
+
- app/controllers
|
|
10
|
+
- app/models
|
|
11
|
+
- app/views
|
|
12
|
+
- app/helpers
|
|
13
|
+
- app/mailers
|
|
14
|
+
- app/jobs
|
|
15
|
+
- app/services
|
|
16
|
+
- app/queries
|
|
17
|
+
- app/presenters
|
|
18
|
+
- app/policies
|
|
19
|
+
- app/serializers
|
|
20
|
+
- app/forms
|
|
21
|
+
- config/initializers
|
|
22
|
+
- db/migrate
|
|
23
|
+
- spec/models
|
|
24
|
+
- spec/controllers
|
|
25
|
+
- spec/requests
|
|
26
|
+
- spec/services
|
|
27
|
+
- spec/policies
|
|
28
|
+
|
|
29
|
+
# Default generators to run after app creation
|
|
30
|
+
generators:
|
|
31
|
+
- model
|
|
32
|
+
- scaffold
|
|
33
|
+
- resource
|
|
34
|
+
|
|
35
|
+
# Features to enable by default
|
|
36
|
+
features:
|
|
37
|
+
- authentication
|
|
38
|
+
- mailers
|
|
39
|
+
|
|
40
|
+
# Template variables
|
|
41
|
+
defaults:
|
|
42
|
+
css_framework: tailwind
|
|
43
|
+
testing: rspec
|
|
44
|
+
database: postgresql
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# ProfileLoader module handles loading RailsForge profiles
|
|
2
|
+
require 'yaml'
|
|
3
|
+
|
|
4
|
+
module RailsForge
|
|
5
|
+
# Profile module for managing application profiles
|
|
6
|
+
module Profile
|
|
7
|
+
# Error class for profile loading issues
|
|
8
|
+
class ProfileError < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Profile directory path
|
|
11
|
+
PROFILES_DIR = File.expand_path('../profiles', __FILE__)
|
|
12
|
+
|
|
13
|
+
# Load a profile by name
|
|
14
|
+
# @param profile_name [String] The name of the profile to load
|
|
15
|
+
# @return [Hash] The profile configuration
|
|
16
|
+
# @raises [ProfileError] If profile not found or invalid
|
|
17
|
+
def self.load(profile_name)
|
|
18
|
+
profile_path = File.join(PROFILES_DIR, "#{profile_name}.yml")
|
|
19
|
+
|
|
20
|
+
unless File.exist?(profile_path)
|
|
21
|
+
raise ProfileError, "Profile '#{profile_name}' not found. Available profiles: #{available_profiles.join(', ')}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
YAML.safe_load(File.read(profile_path), permitted_classes: [], permitted_symbols: [], aliases: true)
|
|
26
|
+
rescue => e
|
|
27
|
+
raise ProfileError, "Failed to load profile '#{profile_name}': #{e.message}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Get list of available profiles
|
|
32
|
+
# @return [Array<String>] Array of profile names
|
|
33
|
+
def self.available_profiles
|
|
34
|
+
return [] unless Dir.exist?(PROFILES_DIR)
|
|
35
|
+
|
|
36
|
+
Dir.glob(File.join(PROFILES_DIR, '*.yml')).map do |file|
|
|
37
|
+
File.basename(file, '.yml')
|
|
38
|
+
end.sort
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Validate a profile configuration
|
|
42
|
+
# @param profile [Hash] The profile configuration to validate
|
|
43
|
+
# @return [Boolean] True if valid
|
|
44
|
+
def self.validate?(profile)
|
|
45
|
+
return false unless profile.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
required_keys = %w[name description folders]
|
|
48
|
+
required_keys.all? { |key| profile.key?(key) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Create folders defined in profile
|
|
52
|
+
# @param base_path [String] The Rails app root path
|
|
53
|
+
# @param profile [Hash] The profile configuration
|
|
54
|
+
def self.create_folders(base_path, profile)
|
|
55
|
+
folders = profile['folders'] || []
|
|
56
|
+
|
|
57
|
+
folders.each do |folder|
|
|
58
|
+
full_path = File.join(base_path, folder)
|
|
59
|
+
unless Dir.exist?(full_path)
|
|
60
|
+
FileUtils.mkdir_p(full_path)
|
|
61
|
+
puts " Created #{folder}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get default features from profile
|
|
67
|
+
# @param profile [Hash] The profile configuration
|
|
68
|
+
# @return [Array<String>] Array of feature names
|
|
69
|
+
def self.default_features(profile)
|
|
70
|
+
profile['features'] || []
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Get default template variables from profile
|
|
74
|
+
# @param profile [Hash] The profile configuration
|
|
75
|
+
# @return [Hash] Default template variables
|
|
76
|
+
def self.defaults(profile)
|
|
77
|
+
profile['defaults'] || {}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Show available profiles with descriptions
|
|
81
|
+
# @return [String] Formatted list of profiles
|
|
82
|
+
def self.show_available
|
|
83
|
+
profiles = available_profiles
|
|
84
|
+
|
|
85
|
+
if profiles.empty?
|
|
86
|
+
return "No profiles available."
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
output = "Available profiles:\n\n"
|
|
90
|
+
|
|
91
|
+
profiles.each do |name|
|
|
92
|
+
profile = load(name)
|
|
93
|
+
output += " #{name.ljust(15)} - #{profile['description']}\n"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
output
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
module RailsForge
|
|
2
|
+
# RefactorAnalyzer module handles refactoring suggestions and code extraction
|
|
3
|
+
module RefactorAnalyzer
|
|
4
|
+
# Error class for refactoring issues
|
|
5
|
+
class RefactorError < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Configuration thresholds
|
|
8
|
+
CONTROLLER_MAX_LINES = 150
|
|
9
|
+
CONTROLLER_MAX_METHODS = 10
|
|
10
|
+
MODEL_MAX_LINES = 200
|
|
11
|
+
MODEL_MAX_METHOD_LINES = 15
|
|
12
|
+
|
|
13
|
+
# Analyzes controllers for refactoring opportunities
|
|
14
|
+
# @param base_path [String] Rails app root path
|
|
15
|
+
# @return [Array<Hash>] Array of refactoring suggestions
|
|
16
|
+
def self.analyze_controllers(base_path = nil)
|
|
17
|
+
base_path ||= find_rails_app_path
|
|
18
|
+
raise RefactorError, "Not in a Rails application directory" unless base_path
|
|
19
|
+
|
|
20
|
+
controllers_dir = File.join(base_path, "app", "controllers")
|
|
21
|
+
return [] unless Dir.exist?(controllers_dir)
|
|
22
|
+
|
|
23
|
+
results = []
|
|
24
|
+
Dir.glob(File.join(controllers_dir, "**", "*_controller.rb")).each do |file|
|
|
25
|
+
result = analyze_controller_file(file)
|
|
26
|
+
results << result if result[:needs_refactoring]
|
|
27
|
+
end
|
|
28
|
+
results
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Analyzes models for refactoring opportunities
|
|
32
|
+
# @param base_path [String] Rails app root path
|
|
33
|
+
# @return [Array<Hash>] Array of refactoring suggestions
|
|
34
|
+
def self.analyze_models(base_path = nil)
|
|
35
|
+
base_path ||= find_rails_app_path
|
|
36
|
+
raise RefactorError, "Not in a Rails application directory" unless base_path
|
|
37
|
+
|
|
38
|
+
models_dir = File.join(base_path, "app", "models")
|
|
39
|
+
return [] unless Dir.exist?(models_dir)
|
|
40
|
+
|
|
41
|
+
results = []
|
|
42
|
+
Dir.glob(File.join(models_dir, "**", "*.rb")).each do |file|
|
|
43
|
+
next if file.end_with?("_application.rb")
|
|
44
|
+
result = analyze_model_file(file)
|
|
45
|
+
results << result if result[:needs_refactoring]
|
|
46
|
+
end
|
|
47
|
+
results
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Analyzes a controller file
|
|
51
|
+
# @param file_path [String] Path to controller file
|
|
52
|
+
# @return [Hash] Analysis results
|
|
53
|
+
def self.analyze_controller_file(file_path)
|
|
54
|
+
content = File.read(file_path)
|
|
55
|
+
lines = content.lines.count
|
|
56
|
+
methods = extract_methods(content)
|
|
57
|
+
|
|
58
|
+
issues = []
|
|
59
|
+
suggestions = []
|
|
60
|
+
|
|
61
|
+
if lines > CONTROLLER_MAX_LINES
|
|
62
|
+
issues << "Controller exceeds #{CONTROLLER_MAX_LINES} lines (currently #{lines})"
|
|
63
|
+
suggestions << "Consider moving business logic into a Service object"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if methods.count > CONTROLLER_MAX_METHODS
|
|
67
|
+
issues << "Controller has #{methods.count} methods (recommended: #{CONTROLLER_MAX_METHODS} or less)"
|
|
68
|
+
suggestions << "Consider extracting some actions into separate controllers or using a Service"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Find long methods that could be extracted
|
|
72
|
+
methods.each do |method|
|
|
73
|
+
if method[:lines] > MODEL_MAX_METHOD_LINES
|
|
74
|
+
suggestions << "Method `#{method[:name]}` has #{method[:lines]} lines - consider extracting to Service"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
type: :controller,
|
|
80
|
+
file: File.basename(file_path),
|
|
81
|
+
path: file_path,
|
|
82
|
+
lines: lines,
|
|
83
|
+
methods: methods,
|
|
84
|
+
issues: issues,
|
|
85
|
+
suggestions: suggestions,
|
|
86
|
+
needs_refactoring: issues.any? || suggestions.any?
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Analyzes a model file
|
|
91
|
+
# @param file_path [String] Path to model file
|
|
92
|
+
# @return [Hash] Analysis results
|
|
93
|
+
def self.analyze_model_file(file_path)
|
|
94
|
+
content = File.read(file_path)
|
|
95
|
+
lines = content.lines.count
|
|
96
|
+
methods = extract_methods(content)
|
|
97
|
+
|
|
98
|
+
issues = []
|
|
99
|
+
suggestions = []
|
|
100
|
+
|
|
101
|
+
if lines > MODEL_MAX_LINES
|
|
102
|
+
issues << "Model exceeds #{MODEL_MAX_LINES} lines (currently #{lines})"
|
|
103
|
+
suggestions << "Consider extracting scopes into Query objects or validations to a Form"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Find long methods
|
|
107
|
+
methods.each do |method|
|
|
108
|
+
if method[:lines] > MODEL_MAX_METHOD_LINES
|
|
109
|
+
suggestions << "Method `#{method[:name]}` has #{method[:lines]} lines - consider extracting to a Service"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
type: :model,
|
|
115
|
+
file: File.basename(file_path),
|
|
116
|
+
path: file_path,
|
|
117
|
+
lines: lines,
|
|
118
|
+
methods: methods,
|
|
119
|
+
issues: issues,
|
|
120
|
+
suggestions: suggestions,
|
|
121
|
+
needs_refactoring: issues.any? || suggestions.any?
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Extracts method names and line counts from content
|
|
126
|
+
# @param content [String] Ruby code content
|
|
127
|
+
# @return [Array<Hash>] Array of method info
|
|
128
|
+
def self.extract_methods(content)
|
|
129
|
+
methods = []
|
|
130
|
+
|
|
131
|
+
# Match def method_name or def self.method_name
|
|
132
|
+
content.scan(/def\s+(self\.)?([a-z_][a-zA-Z_]*)/) do |prefix, name|
|
|
133
|
+
methods << {
|
|
134
|
+
name: name,
|
|
135
|
+
is_class_method: prefix == "self.",
|
|
136
|
+
lines: 1 # Simplified - just mark as present
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
methods
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Counts lines in a method
|
|
144
|
+
# @param content [String] Ruby code content
|
|
145
|
+
# @param method_name [String] Method name
|
|
146
|
+
# @param start_line [Integer] Starting line number
|
|
147
|
+
# @return [Integer] Number of lines
|
|
148
|
+
def self.count_method_lines(content, method_name, start_line)
|
|
149
|
+
# Find the end of the method
|
|
150
|
+
lines = content.lines
|
|
151
|
+
end_pos = content.length
|
|
152
|
+
|
|
153
|
+
# Look for next def or class or end
|
|
154
|
+
rest = content.lines[start_line..-1].join
|
|
155
|
+
if rest =~ /\n\s*def\s+(self\.)?[a-z_]/i
|
|
156
|
+
end_pos = $~.begin(0)
|
|
157
|
+
elsif rest =~ /\n\s*(class|module)\s+/
|
|
158
|
+
end_pos = $~.begin(0)
|
|
159
|
+
elsif rest =~ /\n\s*end\s*$/
|
|
160
|
+
end_pos = $~.begin(0)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
content[start_line..end_pos].lines.count
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Generates a service file for extracted logic
|
|
167
|
+
# @param name [String] Service name
|
|
168
|
+
# @param logic [String] Logic to extract
|
|
169
|
+
# @param base_path [String] Rails app root
|
|
170
|
+
# @return [String] Path to created file
|
|
171
|
+
def self.generate_service(name, logic, base_path = nil)
|
|
172
|
+
base_path ||= find_rails_app_path
|
|
173
|
+
raise RefactorError, "Not in a Rails application directory" unless base_path
|
|
174
|
+
|
|
175
|
+
service_dir = File.join(base_path, "app", "services")
|
|
176
|
+
FileUtils.mkdir_p(service_dir)
|
|
177
|
+
|
|
178
|
+
file_name = "#{name.underscore}_service.rb"
|
|
179
|
+
file_path = File.join(service_dir, file_name)
|
|
180
|
+
|
|
181
|
+
if File.exist?(file_path)
|
|
182
|
+
puts " Skipping service (already exists)"
|
|
183
|
+
return file_path
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
content = <<~RUBY
|
|
187
|
+
# Service class for #{name}
|
|
188
|
+
# Extracted from controller/model logic
|
|
189
|
+
#
|
|
190
|
+
# Usage:
|
|
191
|
+
# #{name}Service.call(params)
|
|
192
|
+
class #{name}Service
|
|
193
|
+
def initialize(**args)
|
|
194
|
+
@args = args
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def call
|
|
198
|
+
# Extracted logic:
|
|
199
|
+
# #{logic.gsub("\n", "\n # ")}
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
RUBY
|
|
203
|
+
|
|
204
|
+
File.write(file_path, content)
|
|
205
|
+
puts " Created app/services/#{file_name}"
|
|
206
|
+
file_path
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Generates a query file for extracted logic
|
|
210
|
+
# @param name [String] Query name
|
|
211
|
+
# @param scope [String] Scope logic
|
|
212
|
+
# @param base_path [String] Rails app root
|
|
213
|
+
# @return [String] Path to created file
|
|
214
|
+
def self.generate_query(name, scope, base_path = nil)
|
|
215
|
+
base_path ||= find_rails_app_path
|
|
216
|
+
raise RefactorError, "Not in a Rails application directory" unless base_path
|
|
217
|
+
|
|
218
|
+
query_dir = File.join(base_path, "app", "queries")
|
|
219
|
+
FileUtils.mkdir_p(query_dir)
|
|
220
|
+
|
|
221
|
+
file_name = "find_#{name.underscore}.rb"
|
|
222
|
+
file_path = File.join(query_dir, file_name)
|
|
223
|
+
|
|
224
|
+
if File.exist?(file_path)
|
|
225
|
+
puts " Skipping query (already exists)"
|
|
226
|
+
return file_path
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
content = <<~RUBY
|
|
230
|
+
# Query class for #{name}
|
|
231
|
+
# Extracted scope/query logic
|
|
232
|
+
#
|
|
233
|
+
# Usage:
|
|
234
|
+
# Find#{name}.call
|
|
235
|
+
class Find#{name}
|
|
236
|
+
def initialize(scope: nil)
|
|
237
|
+
@scope = scope || #{name}.all
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def call
|
|
241
|
+
# Extracted scope:
|
|
242
|
+
# #{scope.gsub("\n", "\n # ")}
|
|
243
|
+
@scope
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
RUBY
|
|
247
|
+
|
|
248
|
+
File.write(file_path, content)
|
|
249
|
+
puts " Created app/queries/#{file_name}"
|
|
250
|
+
file_path
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Generates an RSpec test for a service
|
|
254
|
+
# @param name [String] Service name
|
|
255
|
+
# @param base_path [String] Rails app root
|
|
256
|
+
# @return [String] Path to created file
|
|
257
|
+
def self.generate_service_spec(name, base_path = nil)
|
|
258
|
+
base_path ||= find_rails_app_path
|
|
259
|
+
raise RefactorError, "Not in a Rails application directory" unless base_path
|
|
260
|
+
|
|
261
|
+
spec_dir = File.join(base_path, "spec", "services")
|
|
262
|
+
FileUtils.mkdir_p(spec_dir)
|
|
263
|
+
|
|
264
|
+
file_name = "#{name.underscore}_service_spec.rb"
|
|
265
|
+
file_path = File.join(spec_dir, file_name)
|
|
266
|
+
|
|
267
|
+
if File.exist?(file_path)
|
|
268
|
+
puts " Skipping spec (already exists)"
|
|
269
|
+
return file_path
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
content = <<~RUBY
|
|
273
|
+
require 'rails_helper'
|
|
274
|
+
|
|
275
|
+
RSpec.describe #{name}Service do
|
|
276
|
+
let(:params) { {} }
|
|
277
|
+
subject { described_class.new(params) }
|
|
278
|
+
|
|
279
|
+
describe '#call' do
|
|
280
|
+
it 'returns successful result' do
|
|
281
|
+
expect(subject.call).to be_truthy
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
RUBY
|
|
286
|
+
|
|
287
|
+
File.write(file_path, content)
|
|
288
|
+
puts " Created spec/services/#{file_name}"
|
|
289
|
+
file_path
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Generates an RSpec test for a query
|
|
293
|
+
# @param name [String] Query name
|
|
294
|
+
# @param base_path [String] Rails app root
|
|
295
|
+
# @return [String] Path to created file
|
|
296
|
+
def self.generate_query_spec(name, base_path = nil)
|
|
297
|
+
base_path ||= find_rails_app_path
|
|
298
|
+
raise RefactorError, "Not in a Rails application directory" unless base_path
|
|
299
|
+
|
|
300
|
+
spec_dir = File.join(base_path, "spec", "queries")
|
|
301
|
+
FileUtils.mkdir_p(spec_dir)
|
|
302
|
+
|
|
303
|
+
file_name = "find_#{name.underscore}_spec.rb"
|
|
304
|
+
file_path = File.join(spec_dir, file_name)
|
|
305
|
+
|
|
306
|
+
if File.exist?(file_path)
|
|
307
|
+
puts " Skipping spec (already exists)"
|
|
308
|
+
return file_path
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
content = <<~RUBY
|
|
312
|
+
require 'rails_helper'
|
|
313
|
+
|
|
314
|
+
RSpec.describe Find#{name} do
|
|
315
|
+
let(:scope) { #{name}.all }
|
|
316
|
+
subject { described_class.new(scope: scope) }
|
|
317
|
+
|
|
318
|
+
describe '#call' do
|
|
319
|
+
it 'returns scope' do
|
|
320
|
+
expect(subject.call).to eq(scope)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
RUBY
|
|
325
|
+
|
|
326
|
+
File.write(file_path, content)
|
|
327
|
+
puts " Created spec/queries/#{file_name}"
|
|
328
|
+
file_path
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Extracts code from a controller/model and creates a service
|
|
332
|
+
# @param file_path [String] Source file path
|
|
333
|
+
# @param method_names [Array<String>] Methods to extract
|
|
334
|
+
# @param service_name [String] Name for new service
|
|
335
|
+
# @return [Hash] Results
|
|
336
|
+
def self.extract_to_service(file_path, method_names, service_name)
|
|
337
|
+
content = File.read(file_path)
|
|
338
|
+
|
|
339
|
+
extracted_logic = []
|
|
340
|
+
method_names.each do |method_name|
|
|
341
|
+
# Find method in content
|
|
342
|
+
if content.include?("def #{method_name}")
|
|
343
|
+
# Extract method and its body
|
|
344
|
+
method_match = content.match(/def #{method_name}.*?(\n\s*end\n)/m)
|
|
345
|
+
extracted_logic << method_match[0] if method_match
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
base_path = find_rails_app_path
|
|
350
|
+
generate_service(service_name, extracted_logic.join("\n"), base_path)
|
|
351
|
+
generate_service_spec(service_name, base_path)
|
|
352
|
+
|
|
353
|
+
{
|
|
354
|
+
service: "app/services/#{service_name.underscore}_service.rb",
|
|
355
|
+
spec: "spec/services/#{service_name.underscore}_service_spec.rb"
|
|
356
|
+
}
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Prints refactoring report
|
|
360
|
+
# @param results [Array<Hash>] Analysis results
|
|
361
|
+
def self.print_report(results)
|
|
362
|
+
return puts "\n✓ No refactoring needed!" if results.empty?
|
|
363
|
+
|
|
364
|
+
puts "\n" + "=" * 60
|
|
365
|
+
puts "REFACTORING REPORT"
|
|
366
|
+
puts "=" * 60
|
|
367
|
+
|
|
368
|
+
results.each do |result|
|
|
369
|
+
puts "\n📁 #{result[:file]} (#{result[:type]})"
|
|
370
|
+
puts " Lines: #{result[:lines]}"
|
|
371
|
+
|
|
372
|
+
if result[:issues].any?
|
|
373
|
+
puts "\n ⚠️ Issues:"
|
|
374
|
+
result[:issues].each { |issue| puts " • #{issue}" }
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
if result[:suggestions].any?
|
|
378
|
+
puts "\n 💡 Suggestions:"
|
|
379
|
+
result[:suggestions].each { |sug| puts " • #{sug}" }
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
puts "\n" + "=" * 60
|
|
384
|
+
puts "Total files needing refactoring: #{results.count}"
|
|
385
|
+
puts "=" * 60
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Finds Rails app root path
|
|
389
|
+
# @return [String, nil] Rails app path
|
|
390
|
+
def self.find_rails_app_path
|
|
391
|
+
path = Dir.pwd
|
|
392
|
+
10.times do
|
|
393
|
+
return path if File.exist?(File.join(path, "config", "application.rb"))
|
|
394
|
+
parent = File.dirname(path)
|
|
395
|
+
break if parent == path
|
|
396
|
+
path = parent
|
|
397
|
+
end
|
|
398
|
+
nil
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|