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,313 @@
|
|
|
1
|
+
# Generator module for RailsForge
|
|
2
|
+
# Handles Rails app creation and folder structure
|
|
3
|
+
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
# Generator module handles Rails app creation
|
|
8
|
+
module Generator
|
|
9
|
+
# Folders to create inside the Rails app
|
|
10
|
+
FOLDERS = %w[
|
|
11
|
+
app/services
|
|
12
|
+
app/queries
|
|
13
|
+
app/policies
|
|
14
|
+
app/forms
|
|
15
|
+
app/presenters
|
|
16
|
+
app/jobs
|
|
17
|
+
app/mailers
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
# Error class for invalid app names
|
|
21
|
+
class InvalidAppNameError < StandardError; end
|
|
22
|
+
|
|
23
|
+
# Validates the app name to ensure it's valid for a Rails project
|
|
24
|
+
# @param name [String] The app name to validate
|
|
25
|
+
# @return [void]
|
|
26
|
+
# @raises [InvalidAppNameError] If name is invalid
|
|
27
|
+
def self.validate_app_name(name)
|
|
28
|
+
if name.nil? || name.strip.empty?
|
|
29
|
+
raise InvalidAppNameError, "App name cannot be empty"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Check for valid Ruby/Rails naming conventions
|
|
33
|
+
unless name =~ /\A[a-z][a-z0-9_]*\z/
|
|
34
|
+
raise InvalidAppNameError, "App name must start with a lowercase letter and contain only letters, numbers, and underscores"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Check for reserved Ruby words that would cause issues
|
|
38
|
+
reserved_words = %w[begin end if else elsif unless case when while until do for
|
|
39
|
+
class module def unless return yield break next redo rescue
|
|
40
|
+
require include extend raise attr attr_reader attr_writer
|
|
41
|
+
attr_accessor lambda proc]
|
|
42
|
+
if reserved_words.include?(name)
|
|
43
|
+
raise InvalidAppNameError, "'#{name}' is a reserved Ruby word and cannot be used as an app name"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Creates a new Rails app with the specified name
|
|
48
|
+
# @param app_name [String] The name of the Rails app to create
|
|
49
|
+
# @param options [Hash] Optional flags: auth, admin, jobs
|
|
50
|
+
# @return [String] Success message
|
|
51
|
+
def self.create_app(app_name, options = {})
|
|
52
|
+
validate_app_name(app_name)
|
|
53
|
+
|
|
54
|
+
# Load profile if specified
|
|
55
|
+
profile = nil
|
|
56
|
+
if options[:profile]
|
|
57
|
+
profile = RailsForge::Profile.load(options[:profile])
|
|
58
|
+
puts "Using profile: #{profile['name']} - #{profile['description']}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check if Rails is installed
|
|
62
|
+
unless system("which rails > /dev/null 2>&1")
|
|
63
|
+
raise "Rails is not installed. Please install Rails first: gem install rails"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
puts "Creating new Rails app: #{app_name}..."
|
|
67
|
+
|
|
68
|
+
# Create the Rails app using rails new command
|
|
69
|
+
rails_command = "rails new #{app_name} --skip-git --skip-test --skip-system-test"
|
|
70
|
+
unless system(rails_command)
|
|
71
|
+
raise "Failed to create Rails app. Please check your Rails installation."
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Create folders from profile or defaults
|
|
75
|
+
app_path = File.join(Dir.pwd, app_name)
|
|
76
|
+
if profile
|
|
77
|
+
RailsForge::Profile.create_folders(app_path, profile)
|
|
78
|
+
else
|
|
79
|
+
create_folders(app_path)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Merge profile defaults with options
|
|
83
|
+
if profile
|
|
84
|
+
profile_defaults = RailsForge::Profile.defaults(profile)
|
|
85
|
+
options = profile_defaults.merge(options)
|
|
86
|
+
|
|
87
|
+
# Auto-enable features from profile
|
|
88
|
+
profile_features = RailsForge::Profile.default_features(profile)
|
|
89
|
+
options[:auth] = "devise" if profile_features.include?("authentication") && !options[:auth]
|
|
90
|
+
options[:admin] = "activeadmin" if profile_features.include?("admin") && !options[:admin]
|
|
91
|
+
options[:jobs] = "sidekiq" if profile_features.include?("jobs") && !options[:jobs]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Install optional features based on flags
|
|
95
|
+
results = []
|
|
96
|
+
|
|
97
|
+
if options[:auth]
|
|
98
|
+
results << install_auth(app_path, options[:auth])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
if options[:admin]
|
|
102
|
+
results << install_admin(app_path, options[:admin])
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if options[:jobs]
|
|
106
|
+
results << install_jobs(app_path, options[:jobs])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Build success message
|
|
110
|
+
message = "Rails app '#{app_name}' created successfully!"
|
|
111
|
+
message += "\n\n" + results.join("\n") if results.any?
|
|
112
|
+
message
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Installs authentication (Devise)
|
|
116
|
+
# @param app_path [String] Path to the Rails app
|
|
117
|
+
# @param auth_type [String] Type of auth (devise or jwt)
|
|
118
|
+
# @return [String] Success message
|
|
119
|
+
def self.install_auth(app_path, auth_type)
|
|
120
|
+
puts "\n→ Installing authentication: #{auth_type}..."
|
|
121
|
+
|
|
122
|
+
if auth_type == "devise"
|
|
123
|
+
# Add devise to Gemfile
|
|
124
|
+
gemfile_path = File.join(app_path, "Gemfile")
|
|
125
|
+
if File.exist?(gemfile_path)
|
|
126
|
+
content = File.read(gemfile_path)
|
|
127
|
+
unless content.include?("gem 'devise'")
|
|
128
|
+
File.open(gemfile_path, "a") do |f|
|
|
129
|
+
f.puts "\n# Authentication\ngem 'devise'"
|
|
130
|
+
end
|
|
131
|
+
puts " ✓ Added devise to Gemfile"
|
|
132
|
+
else
|
|
133
|
+
puts " ✓ devise already in Gemfile"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Install devise
|
|
138
|
+
Dir.chdir(app_path) do
|
|
139
|
+
system("bundle install 2>/dev/null")
|
|
140
|
+
system("rails generate devise:install 2>/dev/null")
|
|
141
|
+
system("rails generate devise User 2>/dev/null")
|
|
142
|
+
system("rails db:migrate 2>/dev/null")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
"✅ Authentication (Devise) installed successfully!"
|
|
146
|
+
else
|
|
147
|
+
"⚠️ JWT authentication not implemented yet. Use --auth devise"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Installs admin panel (ActiveAdmin)
|
|
152
|
+
# @param app_path [String] Path to the Rails app
|
|
153
|
+
# @param admin_type [String] Type of admin (activeadmin or dashboard)
|
|
154
|
+
# @return [String] Success message
|
|
155
|
+
def self.install_admin(app_path, admin_type)
|
|
156
|
+
puts "\n→ Installing admin panel: #{admin_type}..."
|
|
157
|
+
|
|
158
|
+
if admin_type == "activeadmin"
|
|
159
|
+
# Add activeadmin and its dependencies to Gemfile
|
|
160
|
+
gemfile_path = File.join(app_path, "Gemfile")
|
|
161
|
+
if File.exist?(gemfile_path)
|
|
162
|
+
content = File.read(gemfile_path)
|
|
163
|
+
unless content.include?("gem 'activeadmin'")
|
|
164
|
+
File.open(gemfile_path, "a") do |f|
|
|
165
|
+
f.puts "\n# Admin panel\ngem 'activeadmin'\ngem 'devise'"
|
|
166
|
+
end
|
|
167
|
+
puts " ✓ Added activeadmin to Gemfile"
|
|
168
|
+
else
|
|
169
|
+
puts " ✓ activeadmin already in Gemfile"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Install activeadmin
|
|
174
|
+
Dir.chdir(app_path) do
|
|
175
|
+
system("bundle install 2>/dev/null")
|
|
176
|
+
system("rails generate active_admin:install 2>/dev/null")
|
|
177
|
+
system("rails db:migrate 2>/dev/null")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Create admin user model if needed
|
|
181
|
+
admin_user_path = File.join(app_path, "app", "models", "admin_user.rb")
|
|
182
|
+
unless File.exist?(admin_user_path)
|
|
183
|
+
File.write(admin_user_path, <<~RUBY)
|
|
184
|
+
# AdminUser model for ActiveAdmin
|
|
185
|
+
class AdminUser < ApplicationRecord
|
|
186
|
+
# Include default devise modules
|
|
187
|
+
devise :database_authenticatable, :recoverable, :rememberable, :validatable
|
|
188
|
+
end
|
|
189
|
+
RUBY
|
|
190
|
+
puts " ✓ Created AdminUser model"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
"✅ Admin panel (ActiveAdmin) installed successfully!"
|
|
194
|
+
else
|
|
195
|
+
"⚠️ Dashboard admin not implemented yet. Use --admin activeadmin"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Installs background jobs (Sidekiq + Redis)
|
|
200
|
+
# @param app_path [String] Path to the Rails app
|
|
201
|
+
# @param jobs_type [String] Type of jobs (sidekiq)
|
|
202
|
+
# @return [String] Success message
|
|
203
|
+
def self.install_jobs(app_path, jobs_type)
|
|
204
|
+
puts "\n→ Installing background jobs: #{jobs_type}..."
|
|
205
|
+
|
|
206
|
+
if jobs_type == "sidekiq"
|
|
207
|
+
# Add sidekiq to Gemfile
|
|
208
|
+
gemfile_path = File.join(app_path, "Gemfile")
|
|
209
|
+
if File.exist?(gemfile_path)
|
|
210
|
+
content = File.read(gemfile_path)
|
|
211
|
+
unless content.include?("gem 'sidekiq'")
|
|
212
|
+
File.open(gemfile_path, "a") do |f|
|
|
213
|
+
f.puts "\n# Background jobs\ngem 'sidekiq'\ngem 'redis', '~> 4.0.1'"
|
|
214
|
+
end
|
|
215
|
+
puts " ✓ Added sidekiq to Gemfile"
|
|
216
|
+
else
|
|
217
|
+
puts " ✓ sidekiq already in Gemfile"
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Install dependencies
|
|
222
|
+
Dir.chdir(app_path) do
|
|
223
|
+
system("bundle install 2>/dev/null")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Create config file for sidekiq
|
|
227
|
+
config_path = File.join(app_path, "config", "sidekiq.yml")
|
|
228
|
+
unless File.exist?(config_path)
|
|
229
|
+
FileUtils.mkdir_p(File.dirname(config_path))
|
|
230
|
+
File.write(config_path, <<~YAML)
|
|
231
|
+
:concurrency: 5
|
|
232
|
+
:queues:
|
|
233
|
+
- default
|
|
234
|
+
- mailers
|
|
235
|
+
YAML
|
|
236
|
+
puts " ✓ Created config/sidekiq.yml"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Create initializers
|
|
240
|
+
init_path = File.join(app_path, "config", "initializers", "sidekiq.rb")
|
|
241
|
+
unless File.exist?(init_path)
|
|
242
|
+
File.write(init_path, <<~RUBY)
|
|
243
|
+
# Sidekiq configuration
|
|
244
|
+
Sidekiq.configure_server do |config|
|
|
245
|
+
config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/1') }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
Sidekiq.configure_client do |config|
|
|
249
|
+
config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/1') }
|
|
250
|
+
end
|
|
251
|
+
RUBY
|
|
252
|
+
puts " ✓ Created config/initializers/sidekiq.rb"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Update application.rb to use sidekiq for active job
|
|
256
|
+
app_rb_path = File.join(app_path, "config", "application.rb")
|
|
257
|
+
if File.exist?(app_rb_path)
|
|
258
|
+
content = File.read(app_rb_path)
|
|
259
|
+
unless content.include?("config.active_job.queue_adapter = :sidekiq")
|
|
260
|
+
content.gsub!(/config\.active_job\.queue_adapter = :.*/, "config.active_job.queue_adapter = :sidekiq")
|
|
261
|
+
File.write(app_rb_path, content)
|
|
262
|
+
puts " ✓ Configured ActiveJob to use Sidekiq"
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Create example job
|
|
267
|
+
jobs_dir = File.join(app_path, "app", "jobs")
|
|
268
|
+
FileUtils.mkdir_p(jobs_dir)
|
|
269
|
+
example_job_path = File.join(jobs_dir, "example_job.rb")
|
|
270
|
+
unless File.exist?(example_job_path)
|
|
271
|
+
File.write(example_job_path, <<~RUBY)
|
|
272
|
+
# Example job using Sidekiq
|
|
273
|
+
class ExampleJob < ApplicationJob
|
|
274
|
+
queue_as :default
|
|
275
|
+
|
|
276
|
+
def perform(*args)
|
|
277
|
+
# Do something later
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
RUBY
|
|
281
|
+
puts " ✓ Created example job"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
"✅ Background jobs (Sidekiq + Redis) installed successfully!"
|
|
285
|
+
else
|
|
286
|
+
"⚠️ Unknown jobs type: #{jobs_type}. Use --jobs sidekiq"
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Creates the additional folder structure inside the Rails app
|
|
291
|
+
# @param app_name [String] The name of the Rails app
|
|
292
|
+
# @return [void]
|
|
293
|
+
def self.create_folders(app_name)
|
|
294
|
+
app_path = File.join(Dir.pwd, app_name)
|
|
295
|
+
|
|
296
|
+
FOLDERS.each do |folder|
|
|
297
|
+
folder_path = File.join(app_path, folder)
|
|
298
|
+
|
|
299
|
+
if Dir.exist?(folder_path)
|
|
300
|
+
puts " Skipping #{folder} (already exists)"
|
|
301
|
+
else
|
|
302
|
+
Dir.mkdir(folder_path)
|
|
303
|
+
puts " Created #{folder}"
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Placeholder for gem functionality
|
|
310
|
+
def self.greet
|
|
311
|
+
"Welcome to RailsForge #{VERSION}!"
|
|
312
|
+
end
|
|
313
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Base generator class for RailsForge
|
|
2
|
+
# All generators inherit from this class
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Generators
|
|
8
|
+
# Base class for all generators
|
|
9
|
+
class BaseGenerator
|
|
10
|
+
# Error class for invalid names
|
|
11
|
+
class InvalidNameError < StandardError; end
|
|
12
|
+
|
|
13
|
+
# Default template version
|
|
14
|
+
TEMPLATE_VERSION = "v1".freeze
|
|
15
|
+
|
|
16
|
+
# Initialize generator
|
|
17
|
+
# @param name [String] Name for the generated object
|
|
18
|
+
# @param options [Hash] Generator options
|
|
19
|
+
def initialize(name, options = {})
|
|
20
|
+
@name = name
|
|
21
|
+
@options = options
|
|
22
|
+
@base_path = find_rails_app_path
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Find Rails app path
|
|
26
|
+
# @return [String, nil] Rails app root path
|
|
27
|
+
def find_rails_app_path
|
|
28
|
+
path = Dir.pwd
|
|
29
|
+
max_depth = 10
|
|
30
|
+
|
|
31
|
+
max_depth.times do
|
|
32
|
+
return path if File.exist?(File.join(path, "config", "application.rb"))
|
|
33
|
+
parent = File.dirname(path)
|
|
34
|
+
break if parent == path
|
|
35
|
+
path = parent
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Validate name
|
|
42
|
+
# @param name [String] Name to validate
|
|
43
|
+
# @param pattern [Regexp] Pattern to match
|
|
44
|
+
# @param error_class [Class] Error class to raise
|
|
45
|
+
def validate_name!(name, pattern, error_class)
|
|
46
|
+
raise error_class, "#{self.class.name.split('::').last} name cannot be empty" if name.nil? || name.strip.empty?
|
|
47
|
+
raise error_class, "Name must match pattern: #{pattern}" unless name =~ pattern
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Convert to underscore
|
|
51
|
+
# @return [String] Underscored name
|
|
52
|
+
def underscore
|
|
53
|
+
@name.to_s.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
54
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
55
|
+
.downcase
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Convert to camelize
|
|
59
|
+
# @return [String] Camelized name
|
|
60
|
+
def camelize
|
|
61
|
+
@name.to_s.split('_').map(&:capitalize).join
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Generate the file
|
|
65
|
+
def generate
|
|
66
|
+
raise NotImplementedError, "Subclasses must implement #generate"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# Demo project generator for RailsForge
|
|
2
|
+
# Creates a complete demo Rails project
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Generators
|
|
8
|
+
# Demo generator creates a sample Rails project
|
|
9
|
+
class DemoGenerator < BaseGenerator
|
|
10
|
+
# Initialize the generator
|
|
11
|
+
def initialize(name, options = {})
|
|
12
|
+
super(name, options)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Generate demo project
|
|
16
|
+
def generate
|
|
17
|
+
return "Not in a Rails application directory" unless @base_path
|
|
18
|
+
|
|
19
|
+
results = []
|
|
20
|
+
results << create_readme
|
|
21
|
+
results << create_sample_services
|
|
22
|
+
results << create_sample_queries
|
|
23
|
+
results << create_sample_jobs
|
|
24
|
+
results << create_sample_serializers
|
|
25
|
+
results << create_sample_policies
|
|
26
|
+
results << create_sample_forms
|
|
27
|
+
|
|
28
|
+
results.join("\n")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Create README.md
|
|
34
|
+
def create_readme
|
|
35
|
+
readme_path = File.join(@base_path, "README.md")
|
|
36
|
+
return "Skipping README (already exists)" if File.exist?(readme_path)
|
|
37
|
+
|
|
38
|
+
content = <<~MD
|
|
39
|
+
# #{@name.titleize}
|
|
40
|
+
|
|
41
|
+
Generated with RailsForge
|
|
42
|
+
|
|
43
|
+
## Setup
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
bundle install
|
|
47
|
+
rails db:create db:migrate
|
|
48
|
+
rails server
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Available Generators
|
|
52
|
+
|
|
53
|
+
Run `railsforge generate --help` to see all available generators.
|
|
54
|
+
|
|
55
|
+
### Services
|
|
56
|
+
```bash
|
|
57
|
+
railsforge generate service UserService
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Queries
|
|
61
|
+
```bash
|
|
62
|
+
railsforge generate query FindUsers
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Jobs
|
|
66
|
+
```bash
|
|
67
|
+
railsforge generate job ProcessData
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Analyzers
|
|
71
|
+
|
|
72
|
+
Run code analysis:
|
|
73
|
+
```bash
|
|
74
|
+
railsforge analyze
|
|
75
|
+
railsforge analyze security
|
|
76
|
+
railsforge analyze performance
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Project Structure
|
|
80
|
+
|
|
81
|
+
- `app/services/` - Service objects
|
|
82
|
+
- `app/queries/` - Query objects
|
|
83
|
+
- `app/jobs/` - Background jobs
|
|
84
|
+
- `app/policies/` - Policy objects
|
|
85
|
+
- `app/forms/` - Form objects
|
|
86
|
+
- `app/serializers/` - JSON serializers
|
|
87
|
+
MD
|
|
88
|
+
|
|
89
|
+
File.write(readme_path, content)
|
|
90
|
+
"Created README.md"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Create sample services
|
|
94
|
+
def create_sample_services
|
|
95
|
+
service_dir = File.join(@base_path, "app", "services")
|
|
96
|
+
FileUtils.mkdir_p(service_dir)
|
|
97
|
+
|
|
98
|
+
samples = %w[user_service notification_service payment_service]
|
|
99
|
+
created = []
|
|
100
|
+
|
|
101
|
+
samples.each do |name|
|
|
102
|
+
path = File.join(service_dir, "#{name}.rb")
|
|
103
|
+
next if File.exist?(path)
|
|
104
|
+
|
|
105
|
+
File.write(path, service_template(name.classify))
|
|
106
|
+
created << name
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
created.any? ? "Created #{created.count} sample services" : "Services already exist"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Create sample queries
|
|
113
|
+
def create_sample_queries
|
|
114
|
+
query_dir = File.join(@base_path, "app", "queries")
|
|
115
|
+
FileUtils.mkdir_p(query_dir)
|
|
116
|
+
|
|
117
|
+
samples = %w[find_active_users find_recent_orders]
|
|
118
|
+
created = []
|
|
119
|
+
|
|
120
|
+
samples.each do |name|
|
|
121
|
+
path = File.join(query_dir, "#{name}.rb")
|
|
122
|
+
next if File.exist?(path)
|
|
123
|
+
|
|
124
|
+
File.write(path, query_template(name.classify))
|
|
125
|
+
created << name
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
created.any? ? "Created #{created.count} sample queries" : "Queries already exist"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Create sample jobs
|
|
132
|
+
def create_sample_jobs
|
|
133
|
+
job_dir = File.join(@base_path, "app", "jobs")
|
|
134
|
+
FileUtils.mkdir_p(job_dir)
|
|
135
|
+
|
|
136
|
+
samples = %w[process_order_job send_email_job]
|
|
137
|
+
created = []
|
|
138
|
+
|
|
139
|
+
samples.each do |name|
|
|
140
|
+
path = File.join(job_dir, "#{name}.rb")
|
|
141
|
+
next if File.exist?(path)
|
|
142
|
+
|
|
143
|
+
File.write(path, job_template(name.classify))
|
|
144
|
+
created << name
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
created.any? ? "Created #{created.count} sample jobs" : "Jobs already exist"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Create sample serializers
|
|
151
|
+
def create_sample_serializers
|
|
152
|
+
serializer_dir = File.join(@base_path, "app", "serializers")
|
|
153
|
+
FileUtils.mkdir_p(serializer_dir)
|
|
154
|
+
|
|
155
|
+
samples = %w[user_serializer article_serializer]
|
|
156
|
+
created = []
|
|
157
|
+
|
|
158
|
+
samples.each do |name|
|
|
159
|
+
path = File.join(serializer_dir, "#{name}.rb")
|
|
160
|
+
next if File.exist?(path)
|
|
161
|
+
|
|
162
|
+
File.write(path, serializer_template(name.classify))
|
|
163
|
+
created << name
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
created.any? ? "Created #{created.count} sample serializers" : "Serializers already exist"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Create sample policies
|
|
170
|
+
def create_sample_policies
|
|
171
|
+
policy_dir = File.join(@base_path, "app", "policies")
|
|
172
|
+
FileUtils.mkdir_p(policy_dir)
|
|
173
|
+
|
|
174
|
+
samples = %w[user_policy article_policy]
|
|
175
|
+
created = []
|
|
176
|
+
|
|
177
|
+
samples.each do |name|
|
|
178
|
+
path = File.join(policy_dir, "#{name}.rb")
|
|
179
|
+
next if File.exist?(path)
|
|
180
|
+
|
|
181
|
+
File.write(path, policy_template(name.classify))
|
|
182
|
+
created << name
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
created.any? ? "Created #{created.count} sample policies" : "Policies already exist"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Create sample forms
|
|
189
|
+
def create_sample_forms
|
|
190
|
+
form_dir = File.join(@base_path, "app", "forms")
|
|
191
|
+
FileUtils.mkdir_p(form_dir)
|
|
192
|
+
|
|
193
|
+
samples = %w[contact_form signup_form]
|
|
194
|
+
created = []
|
|
195
|
+
|
|
196
|
+
samples.each do |name|
|
|
197
|
+
path = File.join(form_dir, "#{name}.rb")
|
|
198
|
+
next if File.exist?(path)
|
|
199
|
+
|
|
200
|
+
File.write(path, form_template(name.classify))
|
|
201
|
+
created << name
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
created.any? ? "Created #{created.count} sample forms" : "Forms already exist"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Templates
|
|
208
|
+
def service_template(name)
|
|
209
|
+
<<~RUBY
|
|
210
|
+
class #{name}
|
|
211
|
+
def initialize(params = {})
|
|
212
|
+
@params = params
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def call
|
|
216
|
+
# TODO: Implement service logic
|
|
217
|
+
{ success: true }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def self.call(params = {})
|
|
221
|
+
new(params).call
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
RUBY
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def query_template(name)
|
|
228
|
+
<<~RUBY
|
|
229
|
+
class #{name}
|
|
230
|
+
def initialize(relation = nil)
|
|
231
|
+
@relation = relation || default_scope
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def call
|
|
235
|
+
@relation
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private
|
|
239
|
+
|
|
240
|
+
def default_scope
|
|
241
|
+
# TODO: Replace with actual model
|
|
242
|
+
OpenStruct.all
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
RUBY
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def job_template(name)
|
|
249
|
+
<<~RUBY
|
|
250
|
+
class #{name} < ApplicationJob
|
|
251
|
+
queue_as :default
|
|
252
|
+
|
|
253
|
+
def perform(*args)
|
|
254
|
+
# TODO: Implement job logic
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
RUBY
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def serializer_template(name)
|
|
261
|
+
<<~RUBY
|
|
262
|
+
class #{name} < ActiveModel::Serializer
|
|
263
|
+
attributes :id, :created_at
|
|
264
|
+
|
|
265
|
+
# TODO: Add attributes
|
|
266
|
+
end
|
|
267
|
+
RUBY
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def policy_template(name)
|
|
271
|
+
<<~RUBY
|
|
272
|
+
class #{name} < ApplicationPolicy
|
|
273
|
+
def index?
|
|
274
|
+
true
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def show?
|
|
278
|
+
true
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def create?
|
|
282
|
+
user.present?
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
RUBY
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def form_template(name)
|
|
289
|
+
<<~RUBY
|
|
290
|
+
class #{name} < ApplicationForm
|
|
291
|
+
attr_accessor :name, :email
|
|
292
|
+
|
|
293
|
+
validates :name, presence: true
|
|
294
|
+
validates :email, presence: true
|
|
295
|
+
|
|
296
|
+
def save
|
|
297
|
+
return false unless valid?
|
|
298
|
+
|
|
299
|
+
# TODO: Implement save logic
|
|
300
|
+
true
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
RUBY
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|