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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +528 -0
  4. data/bin/railsforge +8 -0
  5. data/lib/railsforge/analyzers/base_analyzer.rb +41 -0
  6. data/lib/railsforge/analyzers/controller_analyzer.rb +83 -0
  7. data/lib/railsforge/analyzers/database_analyzer.rb +55 -0
  8. data/lib/railsforge/analyzers/metrics_analyzer.rb +55 -0
  9. data/lib/railsforge/analyzers/model_analyzer.rb +74 -0
  10. data/lib/railsforge/analyzers/performance_analyzer.rb +161 -0
  11. data/lib/railsforge/analyzers/refactor_analyzer.rb +118 -0
  12. data/lib/railsforge/analyzers/security_analyzer.rb +169 -0
  13. data/lib/railsforge/analyzers/spec_analyzer.rb +58 -0
  14. data/lib/railsforge/api_generator.rb +397 -0
  15. data/lib/railsforge/audit.rb +289 -0
  16. data/lib/railsforge/cli.rb +671 -0
  17. data/lib/railsforge/config.rb +181 -0
  18. data/lib/railsforge/database_analyzer.rb +300 -0
  19. data/lib/railsforge/doctor.rb +250 -0
  20. data/lib/railsforge/feature_generator.rb +560 -0
  21. data/lib/railsforge/generator.rb +313 -0
  22. data/lib/railsforge/generators/base_generator.rb +70 -0
  23. data/lib/railsforge/generators/demo_generator.rb +307 -0
  24. data/lib/railsforge/generators/devops_generator.rb +287 -0
  25. data/lib/railsforge/generators/monitoring_generator.rb +134 -0
  26. data/lib/railsforge/generators/service_generator.rb +122 -0
  27. data/lib/railsforge/generators/stimulus_controller_generator.rb +129 -0
  28. data/lib/railsforge/generators/test_generator.rb +289 -0
  29. data/lib/railsforge/generators/view_component_generator.rb +169 -0
  30. data/lib/railsforge/graph.rb +270 -0
  31. data/lib/railsforge/loader.rb +56 -0
  32. data/lib/railsforge/mailer_generator.rb +191 -0
  33. data/lib/railsforge/plugins/plugin_loader.rb +60 -0
  34. data/lib/railsforge/plugins.rb +30 -0
  35. data/lib/railsforge/profiles/admin_app.yml +49 -0
  36. data/lib/railsforge/profiles/api_only.yml +47 -0
  37. data/lib/railsforge/profiles/blog.yml +47 -0
  38. data/lib/railsforge/profiles/standard.yml +44 -0
  39. data/lib/railsforge/profiles.rb +99 -0
  40. data/lib/railsforge/refactor_analyzer.rb +401 -0
  41. data/lib/railsforge/refactor_controller.rb +277 -0
  42. data/lib/railsforge/refactors/refactor_controller.rb +117 -0
  43. data/lib/railsforge/template_loader.rb +105 -0
  44. data/lib/railsforge/templates/v1/form/spec_template.rb +18 -0
  45. data/lib/railsforge/templates/v1/form/template.rb +28 -0
  46. data/lib/railsforge/templates/v1/job/spec_template.rb +17 -0
  47. data/lib/railsforge/templates/v1/job/template.rb +13 -0
  48. data/lib/railsforge/templates/v1/policy/spec_template.rb +41 -0
  49. data/lib/railsforge/templates/v1/policy/template.rb +57 -0
  50. data/lib/railsforge/templates/v1/presenter/spec_template.rb +12 -0
  51. data/lib/railsforge/templates/v1/presenter/template.rb +13 -0
  52. data/lib/railsforge/templates/v1/query/spec_template.rb +12 -0
  53. data/lib/railsforge/templates/v1/query/template.rb +16 -0
  54. data/lib/railsforge/templates/v1/serializer/spec_template.rb +13 -0
  55. data/lib/railsforge/templates/v1/serializer/template.rb +11 -0
  56. data/lib/railsforge/templates/v1/service/spec_template.rb +12 -0
  57. data/lib/railsforge/templates/v1/service/template.rb +25 -0
  58. data/lib/railsforge/templates/v1/stimulus_controller/template.rb +35 -0
  59. data/lib/railsforge/templates/v1/view_component/template.rb +24 -0
  60. data/lib/railsforge/templates/v2/job/template.rb +49 -0
  61. data/lib/railsforge/templates/v2/query/template.rb +66 -0
  62. data/lib/railsforge/templates/v2/service/spec_template.rb +33 -0
  63. data/lib/railsforge/templates/v2/service/template.rb +71 -0
  64. data/lib/railsforge/templates/v3/job/template.rb +72 -0
  65. data/lib/railsforge/templates/v3/query/spec_template.rb +54 -0
  66. data/lib/railsforge/templates/v3/query/template.rb +115 -0
  67. data/lib/railsforge/templates/v3/service/spec_template.rb +51 -0
  68. data/lib/railsforge/templates/v3/service/template.rb +84 -0
  69. data/lib/railsforge/version.rb +5 -0
  70. data/lib/railsforge/wizard.rb +265 -0
  71. data/lib/railsforge/wizard_tui.rb +286 -0
  72. data/lib/railsforge.rb +13 -0
  73. metadata +216 -0
@@ -0,0 +1,560 @@
1
+ module RailsForge
2
+ # FeatureGenerator module handles generating multiple related files for a feature
3
+ module FeatureGenerator
4
+ # Error class for invalid feature names
5
+ class InvalidFeatureNameError < StandardError; end
6
+
7
+ # Validates the feature name to ensure it's valid Ruby class name
8
+ def self.validate_feature_name(name)
9
+ if name.nil? || name.strip.empty?
10
+ raise InvalidFeatureNameError, "Feature name cannot be empty"
11
+ end
12
+
13
+ # Don't validate names starting with dashes (they're likely flags)
14
+ return if name.start_with?("-")
15
+
16
+ unless name =~ /\A[A-Z][a-zA-Z0-9]*\z/
17
+ raise InvalidFeatureNameError, "Feature name must be in PascalCase (e.g., User)"
18
+ end
19
+ end
20
+
21
+ # Generates multiple related files for a feature
22
+ def self.generate(feature_name, with_spec: true)
23
+ validate_feature_name(feature_name)
24
+
25
+ base_path = find_rails_app_path
26
+ unless base_path
27
+ raise "Not in a Rails application directory"
28
+ end
29
+
30
+ results = []
31
+
32
+ # Generate Service
33
+ results << generate_service(base_path, feature_name)
34
+
35
+ # Generate Query
36
+ results << generate_query(base_path, feature_name)
37
+
38
+ # Generate Form
39
+ results << generate_form(base_path, feature_name)
40
+
41
+ # Generate Presenter
42
+ results << generate_presenter(base_path, feature_name)
43
+
44
+ # Generate Policy
45
+ results << generate_policy(base_path, feature_name)
46
+
47
+ # Generate Serializer
48
+ results << generate_serializer(base_path, feature_name)
49
+
50
+ if with_spec
51
+ results << generate_service_spec(base_path, feature_name)
52
+ results << generate_query_spec(base_path, feature_name)
53
+ results << generate_form_spec(base_path, feature_name)
54
+ results << generate_presenter_spec(base_path, feature_name)
55
+ results << generate_policy_spec(base_path, feature_name)
56
+ results << generate_serializer_spec(base_path, feature_name)
57
+ end
58
+
59
+ "Feature '#{feature_name}' generated successfully with #{results.count} files!"
60
+ end
61
+
62
+ def self.generate_service(base_path, feature_name)
63
+ service_dir = File.join(base_path, "app", "services")
64
+ FileUtils.mkdir_p(service_dir)
65
+
66
+ file_name = "#{feature_name.underscore}_service.rb"
67
+ file_path = File.join(service_dir, file_name)
68
+
69
+ return " Skipping service (already exists)" if File.exist?(file_path)
70
+
71
+ File.write(file_path, service_template(feature_name))
72
+ puts " Created app/services/#{file_name}"
73
+ file_path
74
+ end
75
+
76
+ def self.generate_query(base_path, feature_name)
77
+ query_dir = File.join(base_path, "app", "queries")
78
+ FileUtils.mkdir_p(query_dir)
79
+
80
+ file_name = "find_#{feature_name.underscore}.rb"
81
+ file_path = File.join(query_dir, file_name)
82
+
83
+ return " Skipping query (already exists)" if File.exist?(file_path)
84
+
85
+ File.write(file_path, query_template(feature_name))
86
+ puts " Created app/queries/#{file_name}"
87
+ file_path
88
+ end
89
+
90
+ def self.generate_form(base_path, feature_name)
91
+ form_dir = File.join(base_path, "app", "forms")
92
+ FileUtils.mkdir_p(form_dir)
93
+
94
+ file_name = "#{feature_name.underscore}_form.rb"
95
+ file_path = File.join(form_dir, file_name)
96
+
97
+ return " Skipping form (already exists)" if File.exist?(file_path)
98
+
99
+ File.write(file_path, form_template(feature_name))
100
+ puts " Created app/forms/#{file_name}"
101
+ file_path
102
+ end
103
+
104
+ def self.generate_presenter(base_path, feature_name)
105
+ presenter_dir = File.join(base_path, "app", "presenters")
106
+ FileUtils.mkdir_p(presenter_dir)
107
+
108
+ file_name = "#{feature_name.underscore}_presenter.rb"
109
+ file_path = File.join(presenter_dir, file_name)
110
+
111
+ return " Skipping presenter (already exists)" if File.exist?(file_path)
112
+
113
+ File.write(file_path, presenter_template(feature_name))
114
+ puts " Created app/presenters/#{file_name}"
115
+ file_path
116
+ end
117
+
118
+ def self.generate_policy(base_path, feature_name)
119
+ policy_dir = File.join(base_path, "app", "policies")
120
+ FileUtils.mkdir_p(policy_dir)
121
+
122
+ file_name = "#{feature_name.underscore}_policy.rb"
123
+ file_path = File.join(policy_dir, file_name)
124
+
125
+ return " Skipping policy (already exists)" if File.exist?(file_path)
126
+
127
+ File.write(file_path, policy_template(feature_name))
128
+ puts " Created app/policies/#{file_name}"
129
+ file_path
130
+ end
131
+
132
+ def self.generate_serializer(base_path, feature_name)
133
+ serializer_dir = File.join(base_path, "app", "serializers")
134
+ FileUtils.mkdir_p(serializer_dir)
135
+
136
+ file_name = "#{feature_name.underscore}_serializer.rb"
137
+ file_path = File.join(serializer_dir, file_name)
138
+
139
+ return " Skipping serializer (already exists)" if File.exist?(file_path)
140
+
141
+ File.write(file_path, serializer_template(feature_name))
142
+ puts " Created app/serializers/#{file_name}"
143
+ file_path
144
+ end
145
+
146
+ # Spec file generators
147
+ def self.generate_service_spec(base_path, feature_name)
148
+ spec_dir = File.join(base_path, "spec", "services")
149
+ FileUtils.mkdir_p(spec_dir)
150
+
151
+ file_name = "#{feature_name.underscore}_service_spec.rb"
152
+ file_path = File.join(spec_dir, file_name)
153
+
154
+ return " Skipping service spec (already exists)" if File.exist?(file_path)
155
+
156
+ File.write(file_path, service_spec_template(feature_name))
157
+ puts " Created spec/services/#{file_name}"
158
+ file_path
159
+ end
160
+
161
+ def self.generate_query_spec(base_path, feature_name)
162
+ spec_dir = File.join(base_path, "spec", "queries")
163
+ FileUtils.mkdir_p(spec_dir)
164
+
165
+ file_name = "find_#{feature_name.underscore}_spec.rb"
166
+ file_path = File.join(spec_dir, file_name)
167
+
168
+ return " Skipping query spec (already exists)" if File.exist?(file_path)
169
+
170
+ File.write(file_path, query_spec_template(feature_name))
171
+ puts " Created spec/queries/#{file_name}"
172
+ file_path
173
+ end
174
+
175
+ def self.generate_form_spec(base_path, feature_name)
176
+ spec_dir = File.join(base_path, "spec", "forms")
177
+ FileUtils.mkdir_p(spec_dir)
178
+
179
+ file_name = "#{feature_name.underscore}_form_spec.rb"
180
+ file_path = File.join(spec_dir, file_name)
181
+
182
+ return " Skipping form spec (already exists)" if File.exist?(file_path)
183
+
184
+ File.write(file_path, form_spec_template(feature_name))
185
+ puts " Created spec/forms/#{file_name}"
186
+ file_path
187
+ end
188
+
189
+ def self.generate_presenter_spec(base_path, feature_name)
190
+ spec_dir = File.join(base_path, "spec", "presenters")
191
+ FileUtils.mkdir_p(spec_dir)
192
+
193
+ file_name = "#{feature_name.underscore}_presenter_spec.rb"
194
+ file_path = File.join(spec_dir, file_name)
195
+
196
+ return " Skipping presenter spec (already exists)" if File.exist?(file_path)
197
+
198
+ File.write(file_path, presenter_spec_template(feature_name))
199
+ puts " Created spec/presenters/#{file_name}"
200
+ file_path
201
+ end
202
+
203
+ def self.generate_policy_spec(base_path, feature_name)
204
+ spec_dir = File.join(base_path, "spec", "policies")
205
+ FileUtils.mkdir_p(spec_dir)
206
+
207
+ file_name = "#{feature_name.underscore}_policy_spec.rb"
208
+ file_path = File.join(spec_dir, file_name)
209
+
210
+ return " Skipping policy spec (already exists)" if File.exist?(file_path)
211
+
212
+ File.write(file_path, policy_spec_template(feature_name))
213
+ puts " Created spec/policies/#{file_name}"
214
+ file_path
215
+ end
216
+
217
+ def self.generate_serializer_spec(base_path, feature_name)
218
+ spec_dir = File.join(base_path, "spec", "serializers")
219
+ FileUtils.mkdir_p(spec_dir)
220
+
221
+ file_name = "#{feature_name.underscore}_serializer_spec.rb"
222
+ file_path = File.join(spec_dir, file_name)
223
+
224
+ return " Skipping serializer spec (already exists)" if File.exist?(file_path)
225
+
226
+ File.write(file_path, serializer_spec_template(feature_name))
227
+ puts " Created spec/serializers/#{file_name}"
228
+ file_path
229
+ end
230
+
231
+ def self.find_rails_app_path
232
+ path = Dir.pwd
233
+ 10.times do
234
+ return path if File.exist?(File.join(path, "config", "application.rb"))
235
+ parent = File.dirname(path)
236
+ break if parent == path
237
+ path = parent
238
+ end
239
+ nil
240
+ end
241
+
242
+ # Templates
243
+ def self.service_template(feature_name)
244
+ <<~RUBY
245
+ # Service class for #{feature_name}
246
+ # Encapsulates business logic
247
+ #
248
+ # Usage:
249
+ # result = #{feature_name}Service.call(params)
250
+ #
251
+ # @example Basic service
252
+ # class Create#{feature_name}Service
253
+ # def initialize(#{feature_name.underscore}_params)
254
+ # @#{feature_name.underscore}_params = #{feature_name.underscore}_params
255
+ # end
256
+ #
257
+ # def call
258
+ # #{feature_name}.create!(@#{feature_name.underscore}_params)
259
+ # end
260
+ # end
261
+ class #{feature_name}Service
262
+ def initialize(#{feature_name.underscore}_params = {})
263
+ @params = #{feature_name.underscore}_params
264
+ end
265
+
266
+ def call
267
+ # TODO: Implement service logic
268
+ end
269
+ end
270
+ RUBY
271
+ end
272
+
273
+ def self.query_template(feature_name)
274
+ <<~RUBY
275
+ # Query class for #{feature_name}
276
+ # Encapsulates database queries
277
+ #
278
+ # Usage:
279
+ # result = #{feature_name}Query.call
280
+ # result = #{feature_name}Query.call(scope: #{feature_name}.active)
281
+ class Find#{feature_name}
282
+ def initialize(scope: nil)
283
+ @scope = scope || #{feature_name}.all
284
+ end
285
+
286
+ def call
287
+ # TODO: Implement query logic
288
+ @scope
289
+ end
290
+ end
291
+ RUBY
292
+ end
293
+
294
+ def self.form_template(feature_name)
295
+ <<~RUBY
296
+ # Form class for #{feature_name}
297
+ # Encapsulates form validation logic
298
+ #
299
+ # Usage:
300
+ # form = #{feature_name}Form.new(params)
301
+ # if form.valid?
302
+ # form.save
303
+ # end
304
+ class #{feature_name}Form
305
+ include ActiveModel::Model
306
+
307
+ attr_accessor :#{feature_name.underscore}_attributes
308
+
309
+ validate :validate_#{feature_name.underscore}
310
+
311
+ def save
312
+ return false unless valid?
313
+
314
+ # TODO: Implement save logic
315
+ true
316
+ end
317
+
318
+ private
319
+
320
+ def validate_#{feature_name.underscore}
321
+ # TODO: Add validations
322
+ end
323
+ end
324
+ RUBY
325
+ end
326
+
327
+ def self.presenter_template(feature_name)
328
+ <<~RUBY
329
+ # Presenter class for #{feature_name}
330
+ # Handles view presentation logic
331
+ #
332
+ # Usage:
333
+ # presenter = #{feature_name}Presenter.new(@#{feature_name.underscore})
334
+ # presenter.display_name
335
+ class #{feature_name}Presenter
336
+ def initialize(#{feature_name.underscore})
337
+ @#{feature_name.underscore} = #{feature_name.underscore}
338
+ end
339
+
340
+ # TODO: Add presenter methods
341
+ end
342
+ RUBY
343
+ end
344
+
345
+ def self.policy_template(feature_name)
346
+ <<~RUBY
347
+ # Policy class for #{feature_name}
348
+ # Handles authorization logic
349
+ #
350
+ # Usage:
351
+ # authorize #{feature_name.underscore}
352
+ # policy_scope(#{feature_name.underscore})
353
+ class #{feature_name}Policy
354
+ attr_reader :user, :record
355
+
356
+ def initialize(user, record)
357
+ @user = user
358
+ @record = record
359
+ end
360
+
361
+ def index?
362
+ true
363
+ end
364
+
365
+ def show?
366
+ true
367
+ end
368
+
369
+ def create?
370
+ user.present?
371
+ end
372
+
373
+ def new?
374
+ create?
375
+ end
376
+
377
+ def update?
378
+ user.present?
379
+ end
380
+
381
+ def edit?
382
+ update?
383
+ end
384
+
385
+ def destroy?
386
+ user.present?
387
+ end
388
+
389
+ class Scope
390
+ def initialize(user, scope)
391
+ @user = user
392
+ @scope = scope
393
+ end
394
+
395
+ def resolve
396
+ scope.all
397
+ end
398
+
399
+ private
400
+
401
+ attr_reader :user, :scope
402
+ end
403
+ end
404
+ RUBY
405
+ end
406
+
407
+ def self.serializer_template(feature_name)
408
+ <<~RUBY
409
+ # Serializer class for #{feature_name}
410
+ # Handles JSON serialization
411
+ #
412
+ # Usage:
413
+ # render json: #{feature_name}Serializer.new(#{feature_name.underscore})
414
+ class #{feature_name}Serializer < ApplicationSerializer
415
+ attributes :id
416
+
417
+ # TODO: Add custom methods
418
+ end
419
+ RUBY
420
+ end
421
+
422
+ # Spec templates
423
+ def self.service_spec_template(feature_name)
424
+ <<~RUBY
425
+ require 'rails_helper'
426
+
427
+ RSpec.describe #{feature_name}Service do
428
+ let(:params) { {} }
429
+ subject { described_class.new(params) }
430
+
431
+ describe '#call' do
432
+ it 'returns successful result' do
433
+ expect(subject.call).to be_truthy
434
+ end
435
+ end
436
+ end
437
+ RUBY
438
+ end
439
+
440
+ def self.query_spec_template(feature_name)
441
+ <<~RUBY
442
+ require 'rails_helper'
443
+
444
+ RSpec.describe Find#{feature_name} do
445
+ let(:scope) { #{feature_name}.all }
446
+ subject { described_class.new(scope: scope) }
447
+
448
+ describe '#call' do
449
+ it 'returns scope' do
450
+ expect(subject.call).to eq(scope)
451
+ end
452
+ end
453
+ end
454
+ RUBY
455
+ end
456
+
457
+ def self.form_spec_template(feature_name)
458
+ <<~RUBY
459
+ require 'rails_helper'
460
+
461
+ RSpec.describe #{feature_name}Form do
462
+ let(:params) { {} }
463
+ subject { described_class.new(#{feature_name.underscore}_params: params) }
464
+
465
+ describe '#valid?' do
466
+ it 'is valid with valid params' do
467
+ expect(subject.valid?).to be_truthy
468
+ end
469
+ end
470
+
471
+ describe '#save' do
472
+ it 'saves successfully' do
473
+ expect(subject.save).to be_truthy
474
+ end
475
+ end
476
+ end
477
+ RUBY
478
+ end
479
+
480
+ def self.presenter_spec_template(feature_name)
481
+ <<~RUBY
482
+ require 'rails_helper'
483
+
484
+ RSpec.describe #{feature_name}Presenter do
485
+ let(:#{feature_name.underscore}) { #{feature_name}.new }
486
+ subject { described_class.new(#{feature_name.underscore}) }
487
+
488
+ describe 'initialization' do
489
+ it 'initializes successfully' do
490
+ expect(subject).to be_a(described_class)
491
+ end
492
+ end
493
+ end
494
+ RUBY
495
+ end
496
+
497
+ def self.policy_spec_template(feature_name)
498
+ <<~RUBY
499
+ require 'rails_helper'
500
+
501
+ RSpec.describe #{feature_name}Policy do
502
+ let(:user) { nil }
503
+ let(:record) { #{feature_name}.new }
504
+ subject { described_class.new(user, record) }
505
+
506
+ describe '#index?' do
507
+ it 'allows everyone' do
508
+ expect(subject.index?).to be_truthy
509
+ end
510
+ end
511
+
512
+ describe '#show?' do
513
+ it 'allows everyone' do
514
+ expect(subject.show?).to be_truthy
515
+ end
516
+ end
517
+
518
+ describe '#create?' do
519
+ it 'requires user' do
520
+ expect(subject.create?).to be_falsey
521
+ end
522
+
523
+ it 'allows authenticated user' do
524
+ user = User.new
525
+ expect(described_class.new(user, record).create?).to be_truthy
526
+ end
527
+ end
528
+
529
+ describe 'Scope' do
530
+ let(:scope) { #{feature_name}.all }
531
+ subject { described_class::Scope.new(user, scope) }
532
+
533
+ describe '#resolve' do
534
+ it 'returns all records' do
535
+ expect(subject.resolve).to eq(scope.all)
536
+ end
537
+ end
538
+ end
539
+ end
540
+ RUBY
541
+ end
542
+
543
+ def self.serializer_spec_template(feature_name)
544
+ <<~RUBY
545
+ require 'rails_helper'
546
+
547
+ RSpec.describe #{feature_name}Serializer do
548
+ let(:#{feature_name.underscore}) { #{feature_name}.new }
549
+ subject { described_class.new(#{feature_name.underscore}) }
550
+
551
+ describe 'serialization' do
552
+ it 'includes id' do
553
+ expect(subject.as_json[:id]).to eq(#{feature_name.underscore}.id)
554
+ end
555
+ end
556
+ end
557
+ RUBY
558
+ end
559
+ end
560
+ end