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,277 @@
|
|
|
1
|
+
# RefactorController module for RailsForge
|
|
2
|
+
# Automatically refactors fat controllers by extracting service objects
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
# RefactorController class extracts services from fat controller methods
|
|
8
|
+
class RefactorController
|
|
9
|
+
# Minimum lines to consider a method for extraction
|
|
10
|
+
MIN_METHOD_LINES = 15
|
|
11
|
+
|
|
12
|
+
# Initialize the refactor
|
|
13
|
+
def initialize(base_path = nil)
|
|
14
|
+
@base_path = base_path || find_rails_app_path
|
|
15
|
+
raise RefactorControllerError, "Not in a Rails application directory" unless @base_path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Refactor a specific controller
|
|
19
|
+
# @param controller_name [String] Name of controller to refactor
|
|
20
|
+
# @return [Hash] Refactoring results
|
|
21
|
+
def refactor(controller_name)
|
|
22
|
+
controller_file = find_controller_file(controller_name)
|
|
23
|
+
|
|
24
|
+
unless controller_file
|
|
25
|
+
raise RefactorControllerError, "Controller '#{controller_name}' not found"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
puts "Analyzing controller: #{controller_name}"
|
|
29
|
+
puts ""
|
|
30
|
+
|
|
31
|
+
# Read controller content
|
|
32
|
+
content = File.read(controller_file)
|
|
33
|
+
|
|
34
|
+
# Find methods that can be extracted
|
|
35
|
+
long_methods = find_long_methods(content)
|
|
36
|
+
|
|
37
|
+
if long_methods.empty?
|
|
38
|
+
puts "No methods found that need extraction (min #{MIN_METHOD_LINES} lines)"
|
|
39
|
+
return { extracted: [], controller: controller_name }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
puts "Found #{long_methods.count} method(s) to extract:"
|
|
43
|
+
long_methods.each do |method|
|
|
44
|
+
puts " - #{method[:name]} (#{method[:lines]} lines)"
|
|
45
|
+
end
|
|
46
|
+
puts ""
|
|
47
|
+
|
|
48
|
+
# Extract each method to a service
|
|
49
|
+
extracted = []
|
|
50
|
+
long_methods.each do |method|
|
|
51
|
+
service = extract_method_to_service(controller_name, method)
|
|
52
|
+
if service
|
|
53
|
+
update_controller_method(controller_file, method, service)
|
|
54
|
+
extracted << service
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts "Extraction complete!"
|
|
59
|
+
puts " Created #{extracted.count} service(s)"
|
|
60
|
+
puts ""
|
|
61
|
+
|
|
62
|
+
{ extracted: extracted, controller: controller_name, methods: long_methods }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Preview what would be extracted without making changes
|
|
66
|
+
# @param controller_name [String] Name of controller to analyze
|
|
67
|
+
# @return [Array] Methods that would be extracted
|
|
68
|
+
def preview(controller_name)
|
|
69
|
+
controller_file = find_controller_file(controller_name)
|
|
70
|
+
|
|
71
|
+
unless controller_file
|
|
72
|
+
raise RefactorControllerError, "Controller '#{controller_name}' not found"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
content = File.read(controller_file)
|
|
76
|
+
find_long_methods(content)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Find the controller file path
|
|
82
|
+
def find_rails_app_path
|
|
83
|
+
path = Dir.pwd
|
|
84
|
+
max_depth = 10
|
|
85
|
+
|
|
86
|
+
max_depth.times do
|
|
87
|
+
return path if File.exist?(File.join(path, "config", "application.rb"))
|
|
88
|
+
parent = File.dirname(path)
|
|
89
|
+
break if parent == path
|
|
90
|
+
path = parent
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Find controller file by name
|
|
97
|
+
def find_controller_file(controller_name)
|
|
98
|
+
controllers_dir = File.join(@base_path, "app", "controllers")
|
|
99
|
+
|
|
100
|
+
# Try different variations
|
|
101
|
+
[
|
|
102
|
+
"#{controller_name.underscore}_controller.rb",
|
|
103
|
+
"#{controller_name.underscore}_controller.rb"
|
|
104
|
+
].each do |filename|
|
|
105
|
+
path = File.join(controllers_dir, filename)
|
|
106
|
+
return path if File.exist?(path)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Search recursively
|
|
110
|
+
Dir.glob(File.join(controllers_dir, "**", "*_controller.rb")).each do |file|
|
|
111
|
+
return file if File.basename(file).include?(controller_name.underscore)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Find methods that exceed the line threshold
|
|
118
|
+
def find_long_methods(content)
|
|
119
|
+
methods = []
|
|
120
|
+
lines = content.lines
|
|
121
|
+
|
|
122
|
+
# Simple regex to find method definitions
|
|
123
|
+
in_method = false
|
|
124
|
+
method_start = 0
|
|
125
|
+
method_name = nil
|
|
126
|
+
method_lines = []
|
|
127
|
+
|
|
128
|
+
lines.each_with_index do |line, index|
|
|
129
|
+
# Match def method_name or def self.method_name
|
|
130
|
+
if line =~ /\bdef\s+(self\.)?(\w+)/
|
|
131
|
+
if in_method
|
|
132
|
+
# Close previous method
|
|
133
|
+
methods << {
|
|
134
|
+
name: method_name,
|
|
135
|
+
start: method_start,
|
|
136
|
+
lines: method_lines.count,
|
|
137
|
+
content: method_lines.join
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
in_method = true
|
|
142
|
+
method_start = index
|
|
143
|
+
method_name = $2
|
|
144
|
+
method_lines = [line]
|
|
145
|
+
elsif in_method
|
|
146
|
+
if line.strip.start_with?("end") && method_lines.last =~ /\b(end|else|elsif|when)\b/
|
|
147
|
+
# This might be the end of the method
|
|
148
|
+
method_lines << line
|
|
149
|
+
|
|
150
|
+
# Check if this is the closing end
|
|
151
|
+
depth = method_lines.join.scan(/\b(begin|class|module|def|do|if|case)\b/).count
|
|
152
|
+
end_depth = method_lines.join.scan(/\bend\b/).count
|
|
153
|
+
|
|
154
|
+
if depth == end_depth && depth > 0
|
|
155
|
+
methods << {
|
|
156
|
+
name: method_name,
|
|
157
|
+
start: method_start,
|
|
158
|
+
lines: method_lines.count,
|
|
159
|
+
content: method_lines.join
|
|
160
|
+
}
|
|
161
|
+
in_method = false
|
|
162
|
+
method_lines = []
|
|
163
|
+
end
|
|
164
|
+
else
|
|
165
|
+
method_lines << line
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Close last method if still open
|
|
171
|
+
if in_method && method_lines.any?
|
|
172
|
+
methods << {
|
|
173
|
+
name: method_name,
|
|
174
|
+
start: method_start,
|
|
175
|
+
lines: method_lines.count,
|
|
176
|
+
content: method_lines.join
|
|
177
|
+
}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Filter to only long methods
|
|
181
|
+
methods.select { |m| m[:lines] >= MIN_METHOD_LINES }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Extract a method to a service object
|
|
185
|
+
def extract_method_to_service(controller_name, method_info)
|
|
186
|
+
service_dir = File.join(@base_path, "app", "services", controller_name.underscore)
|
|
187
|
+
FileUtils.mkdir_p(service_dir)
|
|
188
|
+
|
|
189
|
+
service_name = "#{method_info[:name]}_service"
|
|
190
|
+
service_class_name = "#{controller_name}#{method_info[:name].camelize}Service"
|
|
191
|
+
|
|
192
|
+
service_path = File.join(service_dir, "#{service_name}.rb")
|
|
193
|
+
|
|
194
|
+
if File.exist?(service_path)
|
|
195
|
+
puts " Skipping #{service_name} (already exists)"
|
|
196
|
+
return nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Generate service content
|
|
200
|
+
service_content = generate_service_content(service_class_name, method_info)
|
|
201
|
+
|
|
202
|
+
File.write(service_path, service_content)
|
|
203
|
+
puts " Created app/services/#{controller_name.underscore}/#{service_name}.rb"
|
|
204
|
+
|
|
205
|
+
{ name: service_name, class: service_class_name, path: service_path }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Generate service object content
|
|
209
|
+
def generate_service_content(class_name, method_info)
|
|
210
|
+
<<~RUBY
|
|
211
|
+
# Service extracted from controller method: #{method_info[:name]}
|
|
212
|
+
# Original method had #{method_info[:lines]} lines
|
|
213
|
+
class #{class_name}
|
|
214
|
+
def initialize(params = {})
|
|
215
|
+
@params = params
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def call
|
|
219
|
+
# TODO: Refactor extracted logic here
|
|
220
|
+
# Original method content:
|
|
221
|
+
# #{method_info[:content].lines.first(5).join('# ')}
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def self.call(params = {})
|
|
225
|
+
new(params).call
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
RUBY
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Update the controller method to use the service
|
|
232
|
+
def update_controller_method(controller_file, method_info, service)
|
|
233
|
+
content = File.read(controller_file)
|
|
234
|
+
|
|
235
|
+
# Replace method body with service call
|
|
236
|
+
new_method = <<~RUBY
|
|
237
|
+
def #{method_info[:name]}
|
|
238
|
+
result = #{service[:class]}.call(params)
|
|
239
|
+
end
|
|
240
|
+
RUBY
|
|
241
|
+
|
|
242
|
+
# Simple replacement - in production would use proper parsing
|
|
243
|
+
lines = content.lines
|
|
244
|
+
|
|
245
|
+
# Find and replace the method
|
|
246
|
+
modified_lines = []
|
|
247
|
+
in_target_method = false
|
|
248
|
+
method_depth = 0
|
|
249
|
+
|
|
250
|
+
lines.each_with_index do |line, index|
|
|
251
|
+
if line =~ /\bdef\s+(self\.)?#{method_info[:name]}\b/
|
|
252
|
+
in_target_method = true
|
|
253
|
+
modified_lines << new_method
|
|
254
|
+
next
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
if in_target_method
|
|
258
|
+
method_depth += 1 if line =~ /\b(def|class|module|if|case|do)\b/
|
|
259
|
+
method_depth -= 1 if line =~ /\bend\b/ && method_depth > 0
|
|
260
|
+
|
|
261
|
+
if method_depth == 0
|
|
262
|
+
in_target_method = false
|
|
263
|
+
end
|
|
264
|
+
next # Skip original method lines
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
modified_lines << line
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
File.write(controller_file, modified_lines.join)
|
|
271
|
+
puts " Updated controller method to use service"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Error class
|
|
275
|
+
class RefactorControllerError < StandardError; end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# RefactorController module for RailsForge
|
|
2
|
+
# Automatically refactors fat controllers by extracting service objects
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Refactors
|
|
8
|
+
# RefactorController class extracts services from fat controller methods
|
|
9
|
+
class RefactorController
|
|
10
|
+
MIN_METHOD_LINES = 15
|
|
11
|
+
|
|
12
|
+
def initialize(base_path = nil)
|
|
13
|
+
@base_path = base_path || find_rails_app_path
|
|
14
|
+
raise RefactorControllerError, "Not in a Rails application" unless @base_path
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Refactor a specific controller
|
|
18
|
+
def refactor(controller_name)
|
|
19
|
+
controller_file = find_controller_file(controller_name)
|
|
20
|
+
raise RefactorControllerError, "Controller not found" unless controller_file
|
|
21
|
+
|
|
22
|
+
content = File.read(controller_file)
|
|
23
|
+
long_methods = find_long_methods(content)
|
|
24
|
+
|
|
25
|
+
if long_methods.empty?
|
|
26
|
+
puts "No methods to extract"
|
|
27
|
+
return { extracted: [] }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
puts "Found #{long_methods.count} method(s) to extract"
|
|
31
|
+
|
|
32
|
+
extracted = []
|
|
33
|
+
long_methods.each do |method|
|
|
34
|
+
service = extract_to_service(controller_name, method)
|
|
35
|
+
extracted << service if service
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
{ extracted: extracted }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def find_rails_app_path
|
|
44
|
+
path = Dir.pwd
|
|
45
|
+
10.times do
|
|
46
|
+
return path if File.exist?(File.join(path, "config", "application.rb"))
|
|
47
|
+
parent = File.dirname(path)
|
|
48
|
+
break if parent == path
|
|
49
|
+
path = parent
|
|
50
|
+
end
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def find_controller_file(name)
|
|
55
|
+
dir = File.join(@base_path, "app", "controllers")
|
|
56
|
+
return nil unless Dir.exist?(dir)
|
|
57
|
+
|
|
58
|
+
Dir.glob(File.join(dir, "**", "*_controller.rb")).find do |f|
|
|
59
|
+
File.basename(f).include?(name.underscore)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def find_long_methods(content)
|
|
64
|
+
methods = []
|
|
65
|
+
lines = content.lines
|
|
66
|
+
in_method = false
|
|
67
|
+
method_lines = []
|
|
68
|
+
method_name = nil
|
|
69
|
+
|
|
70
|
+
lines.each_with_index do |line, i|
|
|
71
|
+
if line =~ /\bdef\s+(\w+)/
|
|
72
|
+
methods << { name: method_name, lines: method_lines.count } if in_method
|
|
73
|
+
in_method = true
|
|
74
|
+
method_name = $1
|
|
75
|
+
method_lines = [line]
|
|
76
|
+
elsif in_method
|
|
77
|
+
method_lines << line
|
|
78
|
+
if line.strip == "end" && method_lines.count > 1
|
|
79
|
+
methods << { name: method_name, lines: method_lines.count, content: method_lines.join }
|
|
80
|
+
in_method = false
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
methods.select { |m| m[:lines] >= MIN_METHOD_LINES }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def extract_to_service(controller_name, method_info)
|
|
89
|
+
service_dir = File.join(@base_path, "app", "services", controller_name.underscore)
|
|
90
|
+
FileUtils.mkdir_p(service_dir)
|
|
91
|
+
|
|
92
|
+
service_name = "#{method_info[:name]}_service"
|
|
93
|
+
service_path = File.join(service_dir, "#{service_name}.rb")
|
|
94
|
+
|
|
95
|
+
return nil if File.exist?(service_path)
|
|
96
|
+
|
|
97
|
+
File.write(service_path, <<~RUBY)
|
|
98
|
+
# Extracted from controller: #{controller_name}
|
|
99
|
+
class #{controller_name}#{method_info[:name].capitalize}Service
|
|
100
|
+
def initialize(params = {})
|
|
101
|
+
@params = params
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def call
|
|
105
|
+
# TODO: Implement extracted logic
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
RUBY
|
|
109
|
+
|
|
110
|
+
puts " Created app/services/#{controller_name.underscore}/#{service_name}.rb"
|
|
111
|
+
{ name: service_name }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
class RefactorControllerError < StandardError; end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# TemplateLoader module handles loading template versions
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
4
|
+
module RailsForge
|
|
5
|
+
# Template module for managing generator templates
|
|
6
|
+
module Template
|
|
7
|
+
# Error class for template loading issues
|
|
8
|
+
class TemplateError < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Template directory path
|
|
11
|
+
TEMPLATES_DIR = File.expand_path('../templates', __FILE__)
|
|
12
|
+
|
|
13
|
+
# Default template version
|
|
14
|
+
DEFAULT_VERSION = "v1"
|
|
15
|
+
|
|
16
|
+
# Get available template versions
|
|
17
|
+
# @return [Array<String>] Array of version names
|
|
18
|
+
def self.available_versions
|
|
19
|
+
return [] unless Dir.exist?(TEMPLATES_DIR)
|
|
20
|
+
|
|
21
|
+
Dir.glob(File.join(TEMPLATES_DIR, 'v*')).map do |dir|
|
|
22
|
+
File.basename(dir)
|
|
23
|
+
end.sort
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Load template for a specific generator and version
|
|
27
|
+
# @param generator_type [String] Type of generator (service, query, etc.)
|
|
28
|
+
# @param version [String] Template version (e.g., "v1")
|
|
29
|
+
# @return [Hash] Template configuration
|
|
30
|
+
def self.load_template(generator_type, version = DEFAULT_VERSION)
|
|
31
|
+
template_path = File.join(TEMPLATES_DIR, version, generator_type, "template.rb")
|
|
32
|
+
|
|
33
|
+
unless File.exist?(template_path)
|
|
34
|
+
raise TemplateError, "Template not found for #{generator_type} version #{version}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
template_path: template_path,
|
|
39
|
+
version: version,
|
|
40
|
+
generator_type: generator_type
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get template content
|
|
45
|
+
# @param generator_type [String] Type of generator
|
|
46
|
+
# @param version [String] Template version
|
|
47
|
+
# @return [String] Template content
|
|
48
|
+
def self.get_content(generator_type, version = DEFAULT_VERSION)
|
|
49
|
+
template_info = load_template(generator_type, version)
|
|
50
|
+
File.read(template_info[:template_path])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check if template exists
|
|
54
|
+
# @param generator_type [String] Type of generator
|
|
55
|
+
# @param version [String] Template version
|
|
56
|
+
# @return [Boolean] True if template exists
|
|
57
|
+
def self.exists?(generator_type, version = DEFAULT_VERSION)
|
|
58
|
+
template_path = File.join(TEMPLATES_DIR, version, generator_type, "template.rb")
|
|
59
|
+
File.exist?(template_path)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# List templates in a version
|
|
63
|
+
# @param version [String] Template version
|
|
64
|
+
# @return [Array<String>] List of generator types
|
|
65
|
+
def self.list_templates(version = DEFAULT_VERSION)
|
|
66
|
+
version_path = File.join(TEMPLATES_DIR, version)
|
|
67
|
+
return [] unless Dir.exist?(version_path)
|
|
68
|
+
|
|
69
|
+
Dir.glob(File.join(version_path, '*')).select do |dir|
|
|
70
|
+
File.directory?(dir)
|
|
71
|
+
end.map do |dir|
|
|
72
|
+
File.basename(dir)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Show available versions with their templates
|
|
77
|
+
# @return [String] Formatted list
|
|
78
|
+
def self.show_available
|
|
79
|
+
output = "Available template versions:\n\n"
|
|
80
|
+
|
|
81
|
+
available_versions.each do |version|
|
|
82
|
+
output += " #{version}:\n"
|
|
83
|
+
templates = list_templates(version)
|
|
84
|
+
templates.each do |template|
|
|
85
|
+
output += " - #{template}\n"
|
|
86
|
+
end
|
|
87
|
+
output += "\n"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
output
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Get spec template content
|
|
94
|
+
# @param generator_type [String] Type of generator
|
|
95
|
+
# @param version [String] Template version
|
|
96
|
+
# @return [String] Spec template content
|
|
97
|
+
def self.get_spec_content(generator_type, version = DEFAULT_VERSION)
|
|
98
|
+
spec_path = File.join(TEMPLATES_DIR, version, generator_type, "spec_template.rb")
|
|
99
|
+
|
|
100
|
+
return nil unless File.exist?(spec_path)
|
|
101
|
+
|
|
102
|
+
File.read(spec_path)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'rails_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe <%= name.camelize %>Form do
|
|
4
|
+
let(:params) { {} }
|
|
5
|
+
subject { described_class.new(<%= name.underscore %>_params: params) }
|
|
6
|
+
|
|
7
|
+
describe '#valid?' do
|
|
8
|
+
it 'is valid with valid params' do
|
|
9
|
+
expect(subject.valid?).to be_truthy
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe '#save' do
|
|
14
|
+
it 'saves successfully' do
|
|
15
|
+
expect(subject.save).to be_truthy
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Form class for <%= name %>
|
|
2
|
+
# Encapsulates form validation logic
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# form = <%= name.camelize %>Form.new(params)
|
|
6
|
+
# if form.valid?
|
|
7
|
+
# form.save
|
|
8
|
+
# end
|
|
9
|
+
class <%= name.camelize %>Form
|
|
10
|
+
include ActiveModel::Model
|
|
11
|
+
|
|
12
|
+
attr_accessor <%= name.underscore %>_attributes
|
|
13
|
+
|
|
14
|
+
validate :validate_<%= name.underscore %>
|
|
15
|
+
|
|
16
|
+
def save
|
|
17
|
+
return false unless valid?
|
|
18
|
+
|
|
19
|
+
# TODO: Implement save logic
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def validate_<%= name.underscore %>
|
|
26
|
+
# TODO: Add validations
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'rails_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe <%= name.camelize %>Job do
|
|
4
|
+
let(:args) { [] }
|
|
5
|
+
|
|
6
|
+
it 'enqueues the job' do
|
|
7
|
+
expect {
|
|
8
|
+
described_class.perform_later(*args)
|
|
9
|
+
}.to have_enqueued_job(described_class)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe '#perform' do
|
|
13
|
+
it 'performs the job' do
|
|
14
|
+
expect { subject.perform(*args) }.not_to raise_error
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Background job for <%= name %>
|
|
2
|
+
# Handles async processing
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# <%= name.camelize %>Job.perform_later(record)
|
|
6
|
+
# <%= name.camelize %>Job.perform_now(record)
|
|
7
|
+
class <%= name.camelize %>Job < ApplicationJob
|
|
8
|
+
queue_as :default
|
|
9
|
+
|
|
10
|
+
def perform(*args)
|
|
11
|
+
# TODO: Implement job logic
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require 'rails_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe <%= name.camelize %>Policy do
|
|
4
|
+
let(:user) { nil }
|
|
5
|
+
let(:record) { <%= name.camelize %>.new }
|
|
6
|
+
subject { described_class.new(user, record) }
|
|
7
|
+
|
|
8
|
+
describe '#index?' do
|
|
9
|
+
it 'allows everyone' do
|
|
10
|
+
expect(subject.index?).to be_truthy
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '#show?' do
|
|
15
|
+
it 'allows everyone' do
|
|
16
|
+
expect(subject.show?).to be_truthy
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe '#create?' do
|
|
21
|
+
it 'requires user' do
|
|
22
|
+
expect(subject.create?).to be_falsey
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'allows authenticated user' do
|
|
26
|
+
user = User.new
|
|
27
|
+
expect(described_class.new(user, record).create?).to be_truthy
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe 'Scope' do
|
|
32
|
+
let(:scope) { <%= name.camelize %>.all }
|
|
33
|
+
subject { described_class::Scope.new(user, scope) }
|
|
34
|
+
|
|
35
|
+
describe '#resolve' do
|
|
36
|
+
it 'returns all records' do
|
|
37
|
+
expect(subject.resolve).to eq(scope.all)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Policy class for <%= name %>
|
|
2
|
+
# Handles authorization logic
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# authorize <%= name.underscore %>
|
|
6
|
+
# policy_scope(<%= name.underscore %>)
|
|
7
|
+
class <%= name.camelize %>Policy
|
|
8
|
+
attr_reader :user, :record
|
|
9
|
+
|
|
10
|
+
def initialize(user, record)
|
|
11
|
+
@user = user
|
|
12
|
+
@record = record
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def index?
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def show?
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create?
|
|
24
|
+
user.present?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def new?
|
|
28
|
+
create?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def update?
|
|
32
|
+
user.present?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def edit?
|
|
36
|
+
update?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def destroy?
|
|
40
|
+
user.present?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class Scope
|
|
44
|
+
def initialize(user, scope)
|
|
45
|
+
@user = user
|
|
46
|
+
@scope = scope
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def resolve
|
|
50
|
+
scope.all
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
attr_reader :user, :scope
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require 'rails_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe <%= name.camelize %>Presenter do
|
|
4
|
+
let(:<%= name.underscore %>) { <%= name.camelize %>.new }
|
|
5
|
+
subject { described_class.new(<%= name.underscore %>) }
|
|
6
|
+
|
|
7
|
+
describe 'initialization' do
|
|
8
|
+
it 'initializes successfully' do
|
|
9
|
+
expect(subject).to be_a(described_class)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Presenter class for <%= name %>
|
|
2
|
+
# Handles view presentation logic
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# presenter = <%= name.camelize %>Presenter.new(@<%= name.underscore %>)
|
|
6
|
+
# presenter.full_name
|
|
7
|
+
class <%= name.camelize %>Presenter
|
|
8
|
+
def initialize(<%= name.underscore %>)
|
|
9
|
+
@<%= name.underscore %> = <%= name.underscore %>
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# TODO: Add presenter methods
|
|
13
|
+
end
|