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,270 @@
|
|
|
1
|
+
# Graph module for RailsForge
|
|
2
|
+
# Generates dependency graphs using Graphviz
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
# Graph class generates visual dependency graphs
|
|
8
|
+
class Graph
|
|
9
|
+
# Initialize the graph generator
|
|
10
|
+
def initialize(base_path = nil)
|
|
11
|
+
@base_path = base_path || find_rails_app_path
|
|
12
|
+
raise GraphError, "Not in a Rails application directory" unless @base_path
|
|
13
|
+
|
|
14
|
+
@output_dir = File.join(@base_path, "tmp", "railsforge")
|
|
15
|
+
@relationships = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Generate dependency graph
|
|
19
|
+
# @return [String] Path to generated files
|
|
20
|
+
def generate
|
|
21
|
+
puts "Analyzing Rails app structure..."
|
|
22
|
+
puts ""
|
|
23
|
+
|
|
24
|
+
# Collect relationships
|
|
25
|
+
analyze_controllers
|
|
26
|
+
analyze_services
|
|
27
|
+
analyze_queries
|
|
28
|
+
analyze_models
|
|
29
|
+
analyze_jobs
|
|
30
|
+
|
|
31
|
+
# Create output directory
|
|
32
|
+
FileUtils.mkdir_p(@output_dir)
|
|
33
|
+
|
|
34
|
+
# Generate DOT file
|
|
35
|
+
dot_file = generate_dot_file
|
|
36
|
+
|
|
37
|
+
# Generate SVG if Graphviz is available
|
|
38
|
+
svg_file = generate_svg(dot_file)
|
|
39
|
+
|
|
40
|
+
puts "Dependency graph generated!"
|
|
41
|
+
puts " DOT: #{dot_file}"
|
|
42
|
+
puts " SVG: #{svg_file}" if svg_file
|
|
43
|
+
|
|
44
|
+
{ dot: dot_file, svg: svg_file }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Find Rails app path
|
|
50
|
+
def find_rails_app_path
|
|
51
|
+
path = Dir.pwd
|
|
52
|
+
max_depth = 10
|
|
53
|
+
|
|
54
|
+
max_depth.times do
|
|
55
|
+
return path if File.exist?(File.join(path, "config", "application.rb"))
|
|
56
|
+
parent = File.dirname(path)
|
|
57
|
+
break if parent == path
|
|
58
|
+
path = parent
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Analyze controllers for dependencies
|
|
65
|
+
def analyze_controllers
|
|
66
|
+
controllers_dir = File.join(@base_path, "app", "controllers")
|
|
67
|
+
return unless Dir.exist?(controllers_dir)
|
|
68
|
+
|
|
69
|
+
Dir.glob(File.join(controllers_dir, "**", "*_controller.rb")).each do |file|
|
|
70
|
+
next if file.end_with?("_application_controller.rb")
|
|
71
|
+
|
|
72
|
+
controller_name = File.basename(file, "_controller.rb").camelize
|
|
73
|
+
content = File.read(file)
|
|
74
|
+
|
|
75
|
+
# Find service dependencies
|
|
76
|
+
content.scan(/(\w+Service)\.call/).each do |service|
|
|
77
|
+
@relationships << { from: controller_name, to: service[0], type: "uses" }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Find query dependencies
|
|
81
|
+
content.scan(/(\w+Query)\.new|(\w+)\.find_/).each do |match|
|
|
82
|
+
query = match[0] || match[1]
|
|
83
|
+
@relationships << { from: controller_name, to: query, type: "queries" }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Find model dependencies
|
|
87
|
+
content.scan(/@(\w+)\s*=/).each do |model|
|
|
88
|
+
@relationships << { from: controller_name, to: model[0].singularize.camelize, type: "uses" }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Find job dependencies
|
|
92
|
+
content.scan(/(\w+Job)\.perform_later/).each do |job|
|
|
93
|
+
@relationships << { from: controller_name, to: job[0], type: "enqueues" }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Analyze services for dependencies
|
|
99
|
+
def analyze_services
|
|
100
|
+
services_dir = File.join(@base_path, "app", "services")
|
|
101
|
+
return unless Dir.exist?(services_dir)
|
|
102
|
+
|
|
103
|
+
Dir.glob(File.join(services_dir, "**", "*.rb")).each do |file|
|
|
104
|
+
service_name = File.basename(file, ".rb").camelize
|
|
105
|
+
content = File.read(file)
|
|
106
|
+
|
|
107
|
+
# Find model dependencies
|
|
108
|
+
content.scan(/(\w+)\.find|(\w+)\.where/).each do |match|
|
|
109
|
+
model = match[0] || match[1]
|
|
110
|
+
@relationships << { from: service_name, to: model.singularize.camelize, type: "queries" }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Find other service dependencies
|
|
114
|
+
content.scan(/(\w+Service)\.call/).each do |service|
|
|
115
|
+
@relationships << { from: service_name, to: service[0], type: "uses" }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Find job dependencies
|
|
119
|
+
content.scan(/(\w+Job)\.perform/).each do |job|
|
|
120
|
+
@relationships << { from: service_name, to: job[0], type: "enqueues" }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Analyze queries for dependencies
|
|
126
|
+
def analyze_queries
|
|
127
|
+
queries_dir = File.join(@base_path, "app", "queries")
|
|
128
|
+
return unless Dir.exist?(queries_dir)
|
|
129
|
+
|
|
130
|
+
Dir.glob(File.join(queries_dir, "**", "*.rb")).each do |file|
|
|
131
|
+
query_name = File.basename(file, ".rb").camelize
|
|
132
|
+
content = File.read(file)
|
|
133
|
+
|
|
134
|
+
# Find model dependencies
|
|
135
|
+
content.scan(/(\w+)\.find|(\w+)\.where/).each do |match|
|
|
136
|
+
model = match[0] || match[1]
|
|
137
|
+
@relationships << { from: query_name, to: model.singularize.camelize, type: "queries" }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Analyze models for associations
|
|
143
|
+
def analyze_models
|
|
144
|
+
models_dir = File.join(@base_path, "app", "models")
|
|
145
|
+
return unless Dir.exist?(models_dir)
|
|
146
|
+
|
|
147
|
+
Dir.glob(File.join(models_dir, "**", "*.rb")).each do |file|
|
|
148
|
+
next if file.end_with?("_application_name = File.basename(file, ".rb.rb")
|
|
149
|
+
|
|
150
|
+
model").camelize
|
|
151
|
+
content = File.read(file)
|
|
152
|
+
|
|
153
|
+
# Find associations
|
|
154
|
+
content.scan(/belongs_to\s+:(\w+)/).each do |assoc|
|
|
155
|
+
@relationships << { from: model_name, to: assoc[0].camelize, type: "belongs_to" }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
content.scan(/has_many\s+:(\w+)/).each do |assoc|
|
|
159
|
+
@relationships << { from: model_name, to: assoc[0].camelize, type: "has_many" }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
content.scan(/has_one\s+:(\w+)/).each do |assoc|
|
|
163
|
+
@relationships << { from: model_name, to: assoc[0].camelize, type: "has_one" }
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Analyze jobs for dependencies
|
|
169
|
+
def analyze_jobs
|
|
170
|
+
jobs_dir = File.join(@base_path, "app", "jobs")
|
|
171
|
+
return unless Dir.exist?(jobs_dir)
|
|
172
|
+
|
|
173
|
+
Dir.glob(File.join(jobs_dir, "**", "*.rb")).each do |file|
|
|
174
|
+
job_name = File.basename(file, ".rb").camelize
|
|
175
|
+
content = File.read(file)
|
|
176
|
+
|
|
177
|
+
# Find service dependencies
|
|
178
|
+
content.scan(/(\w+Service)\.call/).each do |service|
|
|
179
|
+
@relationships << { from: job_name, to: service[0], type: "uses" }
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Generate DOT file
|
|
185
|
+
def generate_dot_file
|
|
186
|
+
dot_path = File.join(@output_dir, "dependency_graph.dot")
|
|
187
|
+
|
|
188
|
+
# Define colors for different types
|
|
189
|
+
type_colors = {
|
|
190
|
+
"controller" => "#3498db",
|
|
191
|
+
"service" => "#2ecc71",
|
|
192
|
+
"query" => "#9b59b6",
|
|
193
|
+
"model" => "#e74c3c",
|
|
194
|
+
"job" => "#f39c12",
|
|
195
|
+
"uses" => "#95a5a6",
|
|
196
|
+
"queries" => "#95a5a6",
|
|
197
|
+
"enqueues" => "#95a5a6",
|
|
198
|
+
"belongs_to" => "#e67e22",
|
|
199
|
+
"has_many" => "#1abc9c",
|
|
200
|
+
"has_one" => "#16a085"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
dot_content = []
|
|
204
|
+
dot_content << "digraph RailsApp {"
|
|
205
|
+
dot_content << " rankdir=LR;"
|
|
206
|
+
dot_content << " node [shape=box, style=rounded];"
|
|
207
|
+
dot_content << ""
|
|
208
|
+
|
|
209
|
+
# Collect all nodes
|
|
210
|
+
nodes = Set.new
|
|
211
|
+
@relationships.each do |rel|
|
|
212
|
+
nodes.add(rel[:from])
|
|
213
|
+
nodes.add(rel[:to])
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Define node styles
|
|
217
|
+
nodes.each do |node|
|
|
218
|
+
type = detect_node_type(node)
|
|
219
|
+
color = type_colors[type] || "#95a5a6"
|
|
220
|
+
dot_content << " \"#{node}\" [fillcolor=\"#{color}\", style=filled, fontcolor=white];"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
dot_content << ""
|
|
224
|
+
|
|
225
|
+
# Define edges
|
|
226
|
+
@relationships.each do |rel|
|
|
227
|
+
style = "solid"
|
|
228
|
+
color = type_colors[rel[:type]] || "#95a5a6"
|
|
229
|
+
arrow = "normal"
|
|
230
|
+
|
|
231
|
+
if %w[belongs_to has_many has_one].include?(rel[:type])
|
|
232
|
+
style = "dashed"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
dot_content << " \"#{rel[:from]}\" -> \"#{rel[:to]}\" [style=#{style}, color=#{color}, arrowhead=#{arrow}];"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
dot_content << "}"
|
|
239
|
+
|
|
240
|
+
File.write(dot_path, dot_content.join("\n"))
|
|
241
|
+
dot_path
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Detect node type based on name
|
|
245
|
+
def detect_node_type(name)
|
|
246
|
+
return "controller" if name.downcase.end_with?("controller")
|
|
247
|
+
return "service" if name.downcase.end_with?("service")
|
|
248
|
+
return "query" if name.downcase.start_with?("find") || name.downcase.end_with?("query")
|
|
249
|
+
return "model" if name.downcase.end_with?("job")
|
|
250
|
+
return "job" if name.downcase.end_with?("job")
|
|
251
|
+
"model"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Generate SVG from DOT file
|
|
255
|
+
def generate_svg(dot_path)
|
|
256
|
+
svg_path = dot_path.sub(".dot", ".svg")
|
|
257
|
+
|
|
258
|
+
# Check if Graphviz is installed
|
|
259
|
+
return nil unless system("which dot > /dev/null 2>&1")
|
|
260
|
+
|
|
261
|
+
system("dot -Tsvg #{dot_path} -o #{svg_path} 2>/dev/null")
|
|
262
|
+
svg_path if File.exist?(svg_path)
|
|
263
|
+
rescue
|
|
264
|
+
nil
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Error class
|
|
268
|
+
class GraphError < StandardError; end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# RailsForge modular loader
|
|
2
|
+
# This file centralizes all require statements
|
|
3
|
+
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
# Get the lib directory path
|
|
7
|
+
LIB_DIR = File.dirname(__FILE__)
|
|
8
|
+
|
|
9
|
+
# Core modules
|
|
10
|
+
require_relative "version"
|
|
11
|
+
require_relative "profiles"
|
|
12
|
+
require_relative "template_loader"
|
|
13
|
+
|
|
14
|
+
# CLI
|
|
15
|
+
require_relative "cli"
|
|
16
|
+
|
|
17
|
+
# Generators (modular)
|
|
18
|
+
require_relative "generators/base_generator"
|
|
19
|
+
require_relative "generators/service_generator"
|
|
20
|
+
require_relative "generators/view_component_generator"
|
|
21
|
+
require_relative "generators/stimulus_controller_generator"
|
|
22
|
+
require_relative "generators/demo_generator"
|
|
23
|
+
require_relative "generators/devops_generator"
|
|
24
|
+
require_relative "generators/monitoring_generator"
|
|
25
|
+
require_relative "generators/test_generator"
|
|
26
|
+
|
|
27
|
+
# Analyzers (modular)
|
|
28
|
+
require_relative "analyzers/base_analyzer"
|
|
29
|
+
require_relative "analyzers/controller_analyzer"
|
|
30
|
+
require_relative "analyzers/model_analyzer"
|
|
31
|
+
require_relative "analyzers/spec_analyzer"
|
|
32
|
+
require_relative "analyzers/database_analyzer"
|
|
33
|
+
require_relative "analyzers/metrics_analyzer"
|
|
34
|
+
require_relative "analyzers/refactor_analyzer"
|
|
35
|
+
require_relative "analyzers/security_analyzer"
|
|
36
|
+
require_relative "analyzers/performance_analyzer"
|
|
37
|
+
|
|
38
|
+
# Refactors
|
|
39
|
+
require_relative "refactors/refactor_controller"
|
|
40
|
+
|
|
41
|
+
# Plugins
|
|
42
|
+
require_relative "plugins"
|
|
43
|
+
|
|
44
|
+
# Additional features (legacy compatibility)
|
|
45
|
+
require_relative "mailer_generator"
|
|
46
|
+
require_relative "feature_generator"
|
|
47
|
+
require_relative "api_generator"
|
|
48
|
+
require_relative "audit"
|
|
49
|
+
require_relative "doctor"
|
|
50
|
+
require_relative "graph"
|
|
51
|
+
require_relative "wizard"
|
|
52
|
+
require_relative "wizard_tui"
|
|
53
|
+
require_relative "config"
|
|
54
|
+
|
|
55
|
+
# Generator module (app creation)
|
|
56
|
+
require_relative "generator"
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
module RailsForge
|
|
2
|
+
# MailerGenerator module handles generating mailers
|
|
3
|
+
module MailerGenerator
|
|
4
|
+
# Error class for invalid mailer names
|
|
5
|
+
class InvalidMailerNameError < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Validates the mailer name to ensure it's valid Ruby class name
|
|
8
|
+
def self.validate_mailer_name(name)
|
|
9
|
+
if name.nil? || name.strip.empty?
|
|
10
|
+
raise InvalidMailerNameError, "Mailer name cannot be empty"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
unless name =~ /\A[A-Z][a-zA-Z0-9]*\z/
|
|
14
|
+
raise InvalidMailerNameError, "Mailer name must be in PascalCase (e.g., UserMailer)"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Generates a mailer file with views and optionally an RSpec test
|
|
19
|
+
def self.generate(mailer_name, with_spec: true, with_jobs: false)
|
|
20
|
+
validate_mailer_name(mailer_name)
|
|
21
|
+
|
|
22
|
+
base_path = find_rails_app_path
|
|
23
|
+
unless base_path
|
|
24
|
+
raise "Not in a Rails application directory"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
generate_mailer_file(base_path, mailer_name, with_jobs)
|
|
28
|
+
generate_view_files(base_path, mailer_name)
|
|
29
|
+
|
|
30
|
+
if with_spec
|
|
31
|
+
generate_spec_file(base_path, mailer_name)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
"Mailer '#{mailer_name}' generated successfully!"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.generate_mailer_file(base_path, mailer_name, with_jobs = false)
|
|
38
|
+
mailer_dir = File.join(base_path, "app", "mailers")
|
|
39
|
+
FileUtils.mkdir_p(mailer_dir)
|
|
40
|
+
|
|
41
|
+
file_name = "#{mailer_name.underscore}.rb"
|
|
42
|
+
file_path = File.join(mailer_dir, file_name)
|
|
43
|
+
|
|
44
|
+
if File.exist?(file_path)
|
|
45
|
+
puts " Skipping mailer (already exists)"
|
|
46
|
+
return file_path
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
File.write(file_path, mailer_template(mailer_name, with_jobs))
|
|
50
|
+
puts " Created app/mailers/#{file_name}"
|
|
51
|
+
file_path
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.generate_view_files(base_path, mailer_name)
|
|
55
|
+
views_dir = File.join(base_path, "app", "views", mailer_name.underscore)
|
|
56
|
+
FileUtils.mkdir_p(views_dir)
|
|
57
|
+
|
|
58
|
+
methods = ["welcome_email", "notification_email"]
|
|
59
|
+
|
|
60
|
+
methods.each do |method|
|
|
61
|
+
html_path = File.join(views_dir, "#{method}.html.erb")
|
|
62
|
+
unless File.exist?(html_path)
|
|
63
|
+
File.write(html_path, view_html_template(mailer_name, method))
|
|
64
|
+
puts " Created app/views/#{mailer_name.underscore}/#{method}.html.erb"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
text_path = File.join(views_dir, "#{method}.text.erb")
|
|
68
|
+
unless File.exist?(text_path)
|
|
69
|
+
File.write(text_path, view_text_template(mailer_name, method))
|
|
70
|
+
puts " Created app/views/#{mailer_name.underscore}/#{method}.text.erb"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.generate_spec_file(base_path, mailer_name)
|
|
76
|
+
spec_dir = File.join(base_path, "spec", "mailers")
|
|
77
|
+
FileUtils.mkdir_p(spec_dir)
|
|
78
|
+
|
|
79
|
+
file_name = "#{mailer_name.underscore}_spec.rb"
|
|
80
|
+
file_path = File.join(spec_dir, file_name)
|
|
81
|
+
|
|
82
|
+
if File.exist?(file_path)
|
|
83
|
+
puts " Skipping spec (already exists)"
|
|
84
|
+
return file_path
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
File.write(file_path, spec_template(mailer_name))
|
|
88
|
+
puts " Created spec/mailers/#{file_name}"
|
|
89
|
+
file_path
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.find_rails_app_path
|
|
93
|
+
path = Dir.pwd
|
|
94
|
+
10.times do
|
|
95
|
+
return path if File.exist?(File.join(path, "config", "application.rb"))
|
|
96
|
+
parent = File.dirname(path)
|
|
97
|
+
break if parent == path
|
|
98
|
+
path = parent
|
|
99
|
+
end
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.mailer_template(mailer_name, with_jobs = false)
|
|
104
|
+
<<~RUBY
|
|
105
|
+
# Mailer class for #{mailer_name}
|
|
106
|
+
# Handles sending emails related to #{mailer_name.underscore}
|
|
107
|
+
#
|
|
108
|
+
# Usage:
|
|
109
|
+
# #{mailer_name}.with(user: @user).welcome_email.deliver_later
|
|
110
|
+
# #{mailer_name}.with(user: @user).notification_email.deliver_now
|
|
111
|
+
class #{mailer_name} < ApplicationMailer
|
|
112
|
+
# Sends a welcome email to the user
|
|
113
|
+
# @param user [User] The user to send the welcome email to
|
|
114
|
+
# @return [Mail::Message] The sent mail message
|
|
115
|
+
def welcome_email(user)
|
|
116
|
+
mail(to: user.email, subject: "Welcome to Our App!")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Sends a notification email to the user
|
|
120
|
+
# @param user [User] The user to send the notification to
|
|
121
|
+
# @param message [String] The notification message
|
|
122
|
+
# @return [Mail::Message] The sent mail message
|
|
123
|
+
def notification_email(user, message: "You have a new notification")
|
|
124
|
+
@user = user
|
|
125
|
+
@message = message
|
|
126
|
+
mail(to: user.email, subject: "Notification from Our App")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Class method to queue email via ActiveJob
|
|
130
|
+
# Use with --jobs flag for background delivery
|
|
131
|
+
# def self.deliver_with_job(user, method: :welcome_email, **args)
|
|
132
|
+
# #{mailer_name}Job.perform_later(user, method: method, **args)
|
|
133
|
+
# end
|
|
134
|
+
end
|
|
135
|
+
RUBY
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def self.view_html_template(mailer_name, method_name)
|
|
139
|
+
<<~ERB
|
|
140
|
+
<!DOCTYPE html>
|
|
141
|
+
<html>
|
|
142
|
+
<head>
|
|
143
|
+
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
|
|
144
|
+
<style>body { font-family: Arial, sans-serif; }</style>
|
|
145
|
+
</head>
|
|
146
|
+
<body>
|
|
147
|
+
<h1>#{method_name == 'welcome_email' ? 'Welcome!' : 'Notification'}</h1>
|
|
148
|
+
#{method_name == 'welcome_email' ? '<p>Welcome to our application!</p>' : '<p><%= @message %></p>'}
|
|
149
|
+
</body>
|
|
150
|
+
</html>
|
|
151
|
+
ERB
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def self.view_text_template(mailer_name, method_name)
|
|
155
|
+
<<~TEXT
|
|
156
|
+
#{method_name == 'welcome_email' ? 'Welcome to our application!' : '<%= @message %>'}
|
|
157
|
+
|
|
158
|
+
--
|
|
159
|
+
Our App
|
|
160
|
+
TEXT
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def self.spec_template(mailer_name)
|
|
164
|
+
<<~RUBY
|
|
165
|
+
require "rails_helper"
|
|
166
|
+
|
|
167
|
+
RSpec.describe #{mailer_name}, type: :mailer do
|
|
168
|
+
describe "welcome_email" do
|
|
169
|
+
let(:user) { User.create!(name: "Test", email: "test@example.com") }
|
|
170
|
+
let(:mail) { described_class.welcome_email(user) }
|
|
171
|
+
|
|
172
|
+
it "renders the headers" do
|
|
173
|
+
expect(mail.subject).to eq("Welcome to Our App!")
|
|
174
|
+
expect(mail.to).to eq([user.email])
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
describe "notification_email" do
|
|
179
|
+
let(:user) { User.create!(name: "Test", email: "test@example.com") }
|
|
180
|
+
let(:mail) { described_class.notification_email(user, message: "Test") }
|
|
181
|
+
|
|
182
|
+
it "renders the body" do
|
|
183
|
+
expect(mail.body.encoded).to include("Test")
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
RUBY
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Plugin loader for RailsForge
|
|
2
|
+
# Handles plugin discovery and loading
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Plugins
|
|
8
|
+
# PluginLoader manages RailsForge plugins
|
|
9
|
+
class PluginLoader
|
|
10
|
+
PLUGINS_DIR = File.expand_path('../../plugins', __FILE__)
|
|
11
|
+
|
|
12
|
+
# List available plugins
|
|
13
|
+
def self.list
|
|
14
|
+
return [] unless Dir.exist?(PLUGINS_DIR)
|
|
15
|
+
|
|
16
|
+
Dir.glob(File.join(PLUGINS_DIR, "*.rb")).map do |file|
|
|
17
|
+
File.basename(file, ".rb")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Load a plugin
|
|
22
|
+
def self.load(plugin_name)
|
|
23
|
+
plugin_path = File.join(PLUGINS_DIR, "#{plugin_name}.rb")
|
|
24
|
+
|
|
25
|
+
unless File.exist?(plugin_path)
|
|
26
|
+
puts "Plugin '#{plugin_name}' not found"
|
|
27
|
+
return false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
require plugin_path
|
|
31
|
+
puts "Plugin '#{plugin_name}' loaded"
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Load all plugins
|
|
36
|
+
def self.load_all
|
|
37
|
+
list.each { |plugin| load(plugin) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Create a new plugin
|
|
41
|
+
def self.create(plugin_name)
|
|
42
|
+
FileUtils.mkdir_p(PLUGINS_DIR)
|
|
43
|
+
|
|
44
|
+
path = File.join(PLUGINS_DIR, "#{plugin_name}.rb")
|
|
45
|
+
return if File.exist?(path)
|
|
46
|
+
|
|
47
|
+
File.write(path, <<~RUBY)
|
|
48
|
+
module RailsForge
|
|
49
|
+
module #{plugin_name.split('_').map(&:capitalize).join}
|
|
50
|
+
PLUGIN_NAME = "#{plugin_name}"
|
|
51
|
+
VERSION = "0.1.0"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
RUBY
|
|
55
|
+
|
|
56
|
+
puts "Created #{path}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# RailsForge Plugins module
|
|
2
|
+
# Provides plugin management functionality
|
|
3
|
+
|
|
4
|
+
require_relative "plugins/plugin_loader"
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Plugins
|
|
8
|
+
# Show installed/available plugins
|
|
9
|
+
def self.show_installed
|
|
10
|
+
plugins = PluginLoader.list
|
|
11
|
+
if plugins.empty?
|
|
12
|
+
puts "No plugins installed."
|
|
13
|
+
puts "Create one with: railsforge plugins create <name>"
|
|
14
|
+
else
|
|
15
|
+
puts "Available plugins:"
|
|
16
|
+
plugins.each { |p| puts " - #{p}" }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Load a plugin by name
|
|
21
|
+
def self.load(plugin_name)
|
|
22
|
+
PluginLoader.load(plugin_name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Create a new plugin
|
|
26
|
+
def self.create(plugin_name)
|
|
27
|
+
PluginLoader.create(plugin_name)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Admin App Profile Configuration
|
|
2
|
+
# Use with: railsforge new my_admin --profile=admin_app
|
|
3
|
+
|
|
4
|
+
name: admin_app
|
|
5
|
+
description: An admin dashboard application with user management, analytics, and CRUD operations
|
|
6
|
+
|
|
7
|
+
# Default folders to create
|
|
8
|
+
folders:
|
|
9
|
+
- app/controllers/admin
|
|
10
|
+
- app/models
|
|
11
|
+
- app/views/admin
|
|
12
|
+
- app/helpers
|
|
13
|
+
- app/services
|
|
14
|
+
- app/queries
|
|
15
|
+
- app/presenters
|
|
16
|
+
- app/policies
|
|
17
|
+
- app/serializers
|
|
18
|
+
- app/forms
|
|
19
|
+
- app/dashboards
|
|
20
|
+
- config/initializers
|
|
21
|
+
- config/environments
|
|
22
|
+
- db/migrate
|
|
23
|
+
- spec/models
|
|
24
|
+
- spec/controllers
|
|
25
|
+
- spec/requests
|
|
26
|
+
- spec/features
|
|
27
|
+
- spec/services
|
|
28
|
+
- spec/policies
|
|
29
|
+
|
|
30
|
+
# Default generators to run after app creation
|
|
31
|
+
generators:
|
|
32
|
+
- model
|
|
33
|
+
- scaffold
|
|
34
|
+
- resource
|
|
35
|
+
|
|
36
|
+
# Features to enable by default
|
|
37
|
+
features:
|
|
38
|
+
- authentication
|
|
39
|
+
- authorization
|
|
40
|
+
- admin
|
|
41
|
+
- pagination
|
|
42
|
+
- export
|
|
43
|
+
|
|
44
|
+
# Template variables
|
|
45
|
+
defaults:
|
|
46
|
+
css_framework: bootstrap
|
|
47
|
+
testing: rspec
|
|
48
|
+
database: postgresql
|
|
49
|
+
admin_framework: activeadmin
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# API Only Profile Configuration
|
|
2
|
+
# Use with: railsforge new my_api --profile=api_only
|
|
3
|
+
|
|
4
|
+
name: api_only
|
|
5
|
+
description: A JSON API application with token authentication and versioning
|
|
6
|
+
|
|
7
|
+
# Default folders to create
|
|
8
|
+
folders:
|
|
9
|
+
- app/controllers/api
|
|
10
|
+
- app/controllers/api/v1
|
|
11
|
+
- app/models
|
|
12
|
+
- app/serializers
|
|
13
|
+
- app/services
|
|
14
|
+
- app/queries
|
|
15
|
+
- app/policies
|
|
16
|
+
- app/forms
|
|
17
|
+
- app/presenters
|
|
18
|
+
- config/initializers
|
|
19
|
+
- config/environments
|
|
20
|
+
- config/initializers/doorkeeper.rb
|
|
21
|
+
- db/migrate
|
|
22
|
+
- spec/models
|
|
23
|
+
- spec/requests
|
|
24
|
+
- spec/api
|
|
25
|
+
- spec/services
|
|
26
|
+
- spec/policies
|
|
27
|
+
- spec/serializers
|
|
28
|
+
|
|
29
|
+
# Default generators to run after app creation
|
|
30
|
+
generators:
|
|
31
|
+
- model
|
|
32
|
+
- resource
|
|
33
|
+
|
|
34
|
+
# Features to enable by default
|
|
35
|
+
features:
|
|
36
|
+
- authentication
|
|
37
|
+
- authorization
|
|
38
|
+
- api
|
|
39
|
+
- versioning
|
|
40
|
+
- token_auth
|
|
41
|
+
|
|
42
|
+
# Template variables
|
|
43
|
+
defaults:
|
|
44
|
+
testing: rspec
|
|
45
|
+
database: postgresql
|
|
46
|
+
api_format: json
|
|
47
|
+
auth_strategy: doorkeeper
|