railsforge 1.0.0 → 1.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a30814a978c1f6eae732253edaf8ebde7fb49b5761ef1927f180a5480f3f7921
4
- data.tar.gz: 4dfd319836fd20dcc79ac23d0c5eeb644802b9f6d480956dbae89cfeb5ef39ed
3
+ metadata.gz: 1ca67789d645855334565249db474d63bfde8607e031642c52a287924c55b9cf
4
+ data.tar.gz: 9517139f8e3224a17ebd875c6606a2ea244e183e289acc1599181eed13522f07
5
5
  SHA512:
6
- metadata.gz: eb2d18edddb6280e6b1eb954efb13a7acedc9e1fc429a7257e59282cbb2225293a0c028b5c34a3b1e1c18f3e7875c1868e4472a8b39b6b90ba6d7e24cd7803b9
7
- data.tar.gz: a8fee69a296ec94b93aecbd6f89223c5fcda26ff37dd6fb96081394dbc7856dd92f5c4a445ac2cd1a7f549c0159ca712f0e0680ad1987dfd166f10bf7a908d1f
6
+ metadata.gz: db1e954e6df040c1856be4cee40357e781201d29a550f31c1dc8924d4c3e93678daecefa42c8e651d02ddef5dc04d4ebd702cb37cac2a0731111ddf0a6e03d13
7
+ data.tar.gz: d7f692d25425099a0ea48499655272a70cf77f517cbfdadbe54826c517aa9e989833774feca6fb7742d36e5aa8bdc47c5c9d9760b8a0130009716ea74298e6c6
data/README.md CHANGED
@@ -1,17 +1,14 @@
1
1
  # RailsForge
2
2
 
3
3
  <p align="center">
4
- <a href="https://github.com/railsforge/railsforge/actions">
5
- <img src="https://img.shields.io/github/actions/workflow/status/railsforge/railsforge/main.yml" alt="CI Status">
4
+ <a href="https://github.com/mfifth/RailsForge/actions">
5
+ <img src="https://img.shields.io/github/actions/workflow/status/mfifth/railsforge/main.yml?label=CI" alt="CI Status">
6
6
  </a>
7
7
  <a href="https://rubygems.org/gems/railsforge">
8
- <img src="https://img.shields.io/gem/v/railsforge" alt="Gem Version">
8
+ <img src="https://img.shields.io/gem/v/railsforge.svg?label=rubygems" alt="Gem Version">
9
9
  </a>
10
- <a href="https://rubygems.org/gems/railsforge">
11
- <img src="https://img.shields.io/gem/dt/railsforge" alt="Gem Downloads">
12
- </a>
13
- <a href="https://github.com/railsforge/railsforge/blob/main/LICENSE">
14
- <img src="https://img.shields.io/github/license/railsforge/railsforge" alt="License">
10
+ <a href="https://github.com/mfifth/RailsForge/blob/main/LICENSE">
11
+ <img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License">
15
12
  </a>
16
13
  </p>
17
14
 
@@ -41,8 +38,7 @@ RailsForge streamlines Rails development by providing automated generators and a
41
38
 
42
39
  ### Requirements
43
40
 
44
- - **Ruby** version 2.7 or higher
45
- - **Rails** version 5.0 or higher (for Rails-specific features)
41
+ - **Ruby** version 3.0 or higher
46
42
  - **Bundler** for gem management
47
43
 
48
44
  ### Setup Steps
@@ -464,7 +460,7 @@ We welcome contributions! Please see our contributing guidelines:
464
460
 
465
461
  ```bash
466
462
  # Fork the repository
467
- git clone https://github.com/your-username/railsforge.git
463
+ git clone https://github.com/mfifth/RailsForge.git
468
464
  cd railsforge
469
465
 
470
466
  # Install dependencies
data/bin/railsforge CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # CLI entrypoint for RailsForge gem
3
- # This file is executable from the command line
3
+ # Uses minimal loader for fast --help and --version
4
4
 
5
5
  require_relative "../lib/railsforge"
6
6
 
7
- # Delegate to RailsForge::CLI
8
- RailsForge::CLI.start(ARGV)
7
+ # Use minimal CLI for fast startup
8
+ RailsForge::CLIMinimal.start(ARGV)
@@ -236,15 +236,29 @@ module RailsForge
236
236
  def self.generate_single(type, name, template_version)
237
237
  case type
238
238
  when :service
239
- "Generating service: #{name}" + "\n" + RailsForge::Generators::ServiceGenerator.generate(name, with_spec: true, template_version: template_version)
239
+ "Generating service: #{name}\n" + RailsForge::Generators::ServiceGenerator.generate(name, with_spec: true, template_version: template_version)
240
240
  when :query
241
- "Generating query: #{name}"
241
+ "Generating query: #{name}\n" + RailsForge::Generators::QueryGenerator.generate(name, with_spec: true, template_version: template_version)
242
242
  when :job
243
- "Generating job: #{name}"
243
+ "Generating job: #{name}\n" + RailsForge::Generators::JobGenerator.generate(name, with_spec: true, template_version: template_version)
244
+ when :form
245
+ "Generating form: #{name}\n" + RailsForge::Generators::FormGenerator.generate(name, with_spec: true, template_version: template_version)
246
+ when :presenter
247
+ "Generating presenter: #{name}\n" + RailsForge::Generators::PresenterGenerator.generate(name, with_spec: true, template_version: template_version)
248
+ when :policy
249
+ "Generating policy: #{name}\n" + RailsForge::Generators::PolicyGenerator.generate(name, with_spec: true, template_version: template_version)
250
+ when :serializer
251
+ "Generating serializer: #{name}\n" + RailsForge::Generators::SerializerGenerator.generate(name, with_spec: true, template_version: template_version)
244
252
  when :component
245
- "Generating component: #{name}" + "\n" + RailsForge::Generators::ViewComponentGenerator.new(name, template_version: template_version).generate
253
+ "Generating component: #{name}\n" + RailsForge::Generators::ViewComponentGenerator.new(name, template_version: template_version).generate
246
254
  when :stimulus
247
- "Generating stimulus: #{name}" + "\n" + RailsForge::Generators::StimulusControllerGenerator.new(name, template_version: template_version).generate
255
+ "Generating stimulus: #{name}\n" + RailsForge::Generators::StimulusControllerGenerator.new(name, template_version: template_version).generate
256
+ when :mailer
257
+ "Generating mailer: #{name}\n" + RailsForge::MailerGenerator.generate(name, with_spec: true)
258
+ when :feature
259
+ "Generating feature: #{name}\n" + RailsForge::FeatureGenerator.generate(name, with_spec: true)
260
+ when :api
261
+ "Generating API: #{name}\n" + RailsForge::Generators::ApiGenerator.generate(name, with_spec: true)
248
262
  else
249
263
  "Generator for #{type} not fully implemented"
250
264
  end
@@ -0,0 +1,93 @@
1
+ # Minimal CLI for fast --help and --version
2
+ # This is loaded first, then full CLI is lazy-loaded when needed
3
+
4
+ module RailsForge
5
+ class CLIMinimal
6
+ def self.start(args)
7
+ command = args.shift
8
+
9
+ case command
10
+ when nil, "--help", "-h"
11
+ display_help
12
+ exit 0
13
+ when "--version", "-v"
14
+ # Only load version, nothing else
15
+ require_relative "version"
16
+ puts "RailsForge #{RailsForge::VERSION}"
17
+ exit 0
18
+ else
19
+ # Load full CLI for other commands - this is where the slowness comes from
20
+ require_relative "cli"
21
+ require_relative "profiles"
22
+ require_relative "template_loader"
23
+ require_relative "generator"
24
+
25
+ # Load all generators
26
+ require_relative "generators/base_generator"
27
+ require_relative "generators/service_generator"
28
+ require_relative "generators/view_component_generator"
29
+ require_relative "generators/stimulus_controller_generator"
30
+ require_relative "generators/demo_generator"
31
+ require_relative "generators/devops_generator"
32
+ require_relative "generators/monitoring_generator"
33
+ require_relative "generators/test_generator"
34
+
35
+ # Load all analyzers
36
+ require_relative "analyzers/base_analyzer"
37
+ require_relative "analyzers/controller_analyzer"
38
+ require_relative "analyzers/model_analyzer"
39
+ require_relative "analyzers/spec_analyzer"
40
+ require_relative "analyzers/database_analyzer"
41
+ require_relative "analyzers/metrics_analyzer"
42
+ require_relative "analyzers/refactor_analyzer"
43
+ require_relative "analyzers/security_analyzer"
44
+ require_relative "analyzers/performance_analyzer"
45
+
46
+ # Load refactors and plugins
47
+ require_relative "refactors/refactor_controller"
48
+ require_relative "plugins"
49
+
50
+ # Load legacy modules
51
+ require_relative "mailer_generator"
52
+ require_relative "feature_generator"
53
+ require_relative "api_generator"
54
+ require_relative "audit"
55
+ require_relative "doctor"
56
+ require_relative "graph"
57
+ require_relative "wizard"
58
+ require_relative "wizard_tui"
59
+ require_relative "config"
60
+
61
+ # Dispatch to full CLI
62
+ CLI.start([command] + args)
63
+ end
64
+ end
65
+
66
+ def self.display_help
67
+ puts <<~HELP
68
+ RailsForge - A Rails development toolkit
69
+
70
+ Usage:
71
+ railsforge new <app_name> Create a new Rails app
72
+ railsforge generate service <name> Generate a service object
73
+ railsforge generate query <name> Generate a query object
74
+ railsforge generate job <name> Generate a job object
75
+ railsforge generate demo <name> Generate a demo project
76
+ railsforge generate devops Generate Docker/CI-CD configs
77
+ railsforge generate monitoring Generate Sentry/Lograge configs
78
+ railsforge generate test <name> Generate test files
79
+ railsforge analyze Analyze code quality
80
+ railsforge doctor Full project health check
81
+ railsforge audit Architecture audit
82
+ railsforge graph Dependency graph
83
+ railsforge refactor Refactoring suggestions
84
+ railsforge wizard Interactive project setup
85
+ railsforge plugins Plugin management
86
+ railsforge config Configuration
87
+ railsforge profiles List available profiles
88
+ railsforge --version, -v Show version
89
+ railsforge --help, -h Show this help
90
+ HELP
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,392 @@
1
+ # API Generator for RailsForge
2
+ # Generates API resources with controllers, serializers, policies, services, and queries
3
+
4
+ require_relative 'base_generator'
5
+
6
+ module RailsForge
7
+ module Generators
8
+ # ApiGenerator creates API resources
9
+ class ApiGenerator < BaseGenerator
10
+ # Error class for invalid resource names
11
+ class InvalidResourceNameError < StandardError; end
12
+
13
+ # Initialize the generator
14
+ # @param name [String] Resource name
15
+ # @param options [Hash] Generator options
16
+ def initialize(name, options = {})
17
+ super(name, options)
18
+ @namespace = options[:namespace] || "api"
19
+ @version = options[:version] || "v1"
20
+ @with_spec = options.fetch(:with_spec, true)
21
+ end
22
+
23
+ # Generate API resource files
24
+ # @return [String] Success message
25
+ def generate
26
+ return "Not in a Rails application directory" unless @base_path
27
+
28
+ validate_name!(@name)
29
+
30
+ results = []
31
+ results << generate_controller
32
+ results << generate_serializer
33
+ results << generate_policy
34
+ results << generate_service
35
+ results << generate_query
36
+ results << generate_request_spec if @with_spec
37
+
38
+ "API resource '#{@name}' generated successfully with #{results.count} files!\n" + results.join("\n")
39
+ end
40
+
41
+ # Class method for CLI
42
+ def self.generate(resource_name, with_spec: true, version: "v1", namespace: "api")
43
+ new(resource_name, with_spec: with_spec, version: version, namespace: namespace).generate
44
+ end
45
+
46
+ private
47
+
48
+ # Validate resource name
49
+ def validate_name!(name)
50
+ raise InvalidResourceNameError, "Resource name cannot be empty" if name.nil? || name.strip.empty?
51
+ raise InvalidResourceNameError, "Name must match pattern: /\\A[A-Z][a-zA-Z0-9]*\\z/" unless name =~ /\A[A-Z][a-zA-Z0-9]*\z/
52
+ end
53
+
54
+ # Pluralize helper
55
+ def pluralize(word)
56
+ return word + 's' unless word.end_with?('s')
57
+ return word + 'es' if word.end_with?('sh') || word.end_with?('ch')
58
+ word + 's'
59
+ end
60
+
61
+ # Generate controller
62
+ def generate_controller
63
+ controller_dir = File.join(@base_path, "app", "controllers", @namespace, @version)
64
+ FileUtils.mkdir_p(controller_dir)
65
+
66
+ resource_plural = pluralize(@name)
67
+ resource_underscore = underscore
68
+ resource_underscore_plural = pluralize(resource_underscore)
69
+
70
+ file_name = "#{resource_underscore}_controller.rb"
71
+ file_path = File.join(controller_dir, file_name)
72
+
73
+ return " Skipping controller (already exists)" if File.exist?(file_path)
74
+
75
+ content = <<~RUBY
76
+ # API Controller for #{@name}
77
+ # Version: #{@version}
78
+ # Namespace: #{@namespace}
79
+ #
80
+ # Generates standard CRUD actions for REST API
81
+ class #{resource_plural}Controller < ApplicationController
82
+ before_action :set_#{resource_underscore}, only: [:show, :update, :destroy]
83
+ before_action :authenticate_user!, unless: :devise_controller?
84
+
85
+ # GET /#{resource_underscore_plural}
86
+ def index
87
+ @#{resource_underscore_plural} = #{@name}Query.call
88
+ render json: #{@name}Serializer.new(@#{resource_underscore_plural}).serializable_hash
89
+ end
90
+
91
+ # GET /#{resource_underscore_plural}/:id
92
+ def show
93
+ render json: #{@name}Serializer.new(@#{resource_underscore}).serializable_hash
94
+ end
95
+
96
+ # POST /#{resource_underscore_plural}
97
+ def create
98
+ @result = Create#{@name}Service.call(#{resource_underscore}_params)
99
+
100
+ if @result.success?
101
+ render json: #{@name}Serializer.new(@result.#{resource_underscore}).serializable_hash, status: :created
102
+ else
103
+ render json: { errors: @result.errors }, status: :unprocessable_entity
104
+ end
105
+ end
106
+
107
+ # PATCH/PUT /#{resource_underscore_plural}/:id
108
+ def update
109
+ @result = Update#{@name}Service.call(@#{resource_underscore}, #{resource_underscore}_params)
110
+
111
+ if @result.success?
112
+ render json: #{@name}Serializer.new(@result.#{resource_underscore}).serializable_hash
113
+ else
114
+ render json: { errors: @result.errors }, status: :unprocessable_entity
115
+ end
116
+ end
117
+
118
+ # DELETE /#{resource_underscore_plural}/:id
119
+ def destroy
120
+ @#{resource_underscore}.destroy
121
+ head :no_content
122
+ end
123
+
124
+ private
125
+
126
+ def set_#{resource_underscore}
127
+ @#{resource_underscore} = #{@name}.find(params[:id])
128
+ end
129
+
130
+ def #{resource_underscore}_params
131
+ params.require(:#{resource_underscore}).permit(:name)
132
+ end
133
+ end
134
+ RUBY
135
+
136
+ File.write(file_path, content)
137
+ " Created app/controllers/#{@namespace}/#{@version}/#{file_name}"
138
+ end
139
+
140
+ # Generate serializer
141
+ def generate_serializer
142
+ serializer_dir = File.join(@base_path, "app", "serializers")
143
+ FileUtils.mkdir_p(serializer_dir)
144
+
145
+ file_name = "#{underscore}_serializer.rb"
146
+ file_path = File.join(serializer_dir, file_name)
147
+
148
+ return " Skipping serializer (already exists)" if File.exist?(file_path)
149
+
150
+ content = <<~RUBY
151
+ # Serializer for #{@name}
152
+ class #{@name}Serializer < ApplicationSerializer
153
+ attributes :id, :name, :created_at, :updated_at
154
+ end
155
+ RUBY
156
+
157
+ File.write(file_path, content)
158
+ " Created app/serializers/#{file_name}"
159
+ end
160
+
161
+ # Generate policy
162
+ def generate_policy
163
+ policy_dir = File.join(@base_path, "app", "policies")
164
+ FileUtils.mkdir_p(policy_dir)
165
+
166
+ file_name = "#{underscore}_policy.rb"
167
+ file_path = File.join(policy_dir, file_name)
168
+
169
+ return " Skipping policy (already exists)" if File.exist?(file_path)
170
+
171
+ content = <<~RUBY
172
+ # Policy for #{@name}
173
+ class #{@name}Policy
174
+ attr_reader :user, :record
175
+
176
+ def initialize(user, record)
177
+ @user = user
178
+ @record = record
179
+ end
180
+
181
+ def index?
182
+ true
183
+ end
184
+
185
+ def show?
186
+ true
187
+ end
188
+
189
+ def create?
190
+ user.present?
191
+ end
192
+
193
+ def update?
194
+ user.present?
195
+ end
196
+
197
+ def destroy?
198
+ user.present?
199
+ end
200
+
201
+ class Scope
202
+ def initialize(user, scope)
203
+ @user = user
204
+ @scope = scope
205
+ end
206
+
207
+ def resolve
208
+ scope.all
209
+ end
210
+ end
211
+ end
212
+ RUBY
213
+
214
+ File.write(file_path, content)
215
+ " Created app/policies/#{file_name}"
216
+ end
217
+
218
+ # Generate service
219
+ def generate_service
220
+ service_dir = File.join(@base_path, "app", "services")
221
+ FileUtils.mkdir_p(service_dir)
222
+ res_underscore = underscore
223
+
224
+ # Create service
225
+ file_name = "create_#{res_underscore}_service.rb"
226
+ file_path = File.join(service_dir, file_name)
227
+
228
+ unless File.exist?(file_path)
229
+ content = <<~RUBY
230
+ # Service for creating #{@name}
231
+ class Create#{@name}Service
232
+ def initialize(params)
233
+ @params = params
234
+ end
235
+
236
+ def call
237
+ @#{res_underscore} = #{@name}.create!(@params)
238
+ Result.new(success: true, #{res_underscore}: @#{res_underscore})
239
+ rescue => e
240
+ Result.new(success: false, errors: e.message)
241
+ end
242
+
243
+ class Result
244
+ attr_reader :#{res_underscore}, :errors
245
+ def initialize(success:, #{res_underscore}: nil, errors: nil)
246
+ @success = success
247
+ @#{res_underscore} = #{res_underscore}
248
+ @errors = errors
249
+ end
250
+
251
+ def success?
252
+ @success
253
+ end
254
+ end
255
+ end
256
+ RUBY
257
+
258
+ File.write(file_path, content)
259
+ end
260
+
261
+ # Update service
262
+ file_name = "update_#{res_underscore}_service.rb"
263
+ file_path = File.join(service_dir, file_name)
264
+
265
+ unless File.exist?(file_path)
266
+ content = <<~RUBY
267
+ # Service for updating #{@name}
268
+ class Update#{@name}Service
269
+ def initialize(#{res_underscore}, params)
270
+ @#{res_underscore} = #{res_underscore}
271
+ @params = params
272
+ end
273
+
274
+ def call
275
+ if @#{res_underscore}.update(@params)
276
+ Result.new(success: true, #{res_underscore}: @#{res_underscore})
277
+ else
278
+ Result.new(success: false, errors: @#{res_underscore}.errors.full_messages)
279
+ end
280
+ end
281
+
282
+ class Result
283
+ attr_reader :#{res_underscore}, :errors
284
+ def initialize(success:, #{res_underscore}: nil, errors: nil)
285
+ @success = success
286
+ @#{res_underscore} = #{res_underscore}
287
+ @errors = errors
288
+ end
289
+
290
+ def success?
291
+ @success
292
+ end
293
+ end
294
+ end
295
+ RUBY
296
+
297
+ File.write(file_path, content)
298
+ end
299
+
300
+ " Created app/services/create_#{res_underscore}_service.rb and update_#{res_underscore}_service.rb"
301
+ end
302
+
303
+ # Generate query
304
+ def generate_query
305
+ query_dir = File.join(@base_path, "app", "queries")
306
+ FileUtils.mkdir_p(query_dir)
307
+
308
+ file_name = "find_#{underscore}.rb"
309
+ file_path = File.join(query_dir, file_name)
310
+
311
+ return " Skipping query (already exists)" if File.exist?(file_path)
312
+
313
+ content = <<~RUBY
314
+ # Query for finding #{@name}
315
+ class Find#{@name}
316
+ def initialize(scope: nil)
317
+ @scope = scope || #{@name}.all
318
+ end
319
+
320
+ def call
321
+ @scope
322
+ end
323
+ end
324
+ RUBY
325
+
326
+ File.write(file_path, content)
327
+ " Created app/queries/#{file_name}"
328
+ end
329
+
330
+ # Generate request spec
331
+ def generate_request_spec
332
+ spec_dir = File.join(@base_path, "spec", "requests", @namespace, @version)
333
+ FileUtils.mkdir_p(spec_dir)
334
+
335
+ resource_plural = pluralize(@name)
336
+ resource_underscore = underscore
337
+
338
+ file_name = "#{resource_underscore}_spec.rb"
339
+ file_path = File.join(spec_dir, file_name)
340
+
341
+ return " Skipping spec (already exists)" if File.exist?(file_path)
342
+
343
+ content = <<~RUBY
344
+ require 'rails_helper'
345
+
346
+ RSpec.describe "#{@namespace.capitalize}::#{@version.capitalize}::#{resource_plural}Controller" do
347
+ let(:user) { User.create!(name: "Test", email: "test@example.com") }
348
+ let(:#{resource_underscore}) { #{@name}.create!(name: "Test #{@name}") }
349
+
350
+ before do
351
+ sign_in user
352
+ end
353
+
354
+ describe "GET /index" do
355
+ it "returns a success response" do
356
+ get :index
357
+ expect(response).to be_successful
358
+ end
359
+ end
360
+
361
+ describe "GET /show" do
362
+ it "returns a success response" do
363
+ get :show, params: { id: #{resource_underscore}.id }
364
+ expect(response).to be_successful
365
+ end
366
+ end
367
+
368
+ describe "POST /create" do
369
+ it "creates a new #{resource_underscore}" do
370
+ expect {
371
+ post :create, params: { #{resource_underscore}: { name: "New #{@name}" } }
372
+ }.to change(#{@name}, :count).by(1)
373
+ end
374
+ end
375
+
376
+ describe "DELETE /destroy" do
377
+ it "deletes the #{resource_underscore}" do
378
+ #{resource_underscore}
379
+ expect {
380
+ delete :destroy, params: { id: #{resource_underscore}.id }
381
+ }.to change(#{@name}, :count).by(-1)
382
+ end
383
+ end
384
+ end
385
+ RUBY
386
+
387
+ File.write(file_path, content)
388
+ " Created spec/requests/#{@namespace}/#{@version}/#{file_name}"
389
+ end
390
+ end
391
+ end
392
+ end
@@ -58,7 +58,12 @@ module RailsForge
58
58
  # Convert to camelize
59
59
  # @return [String] Camelized name
60
60
  def camelize
61
- @name.to_s.split('_').map(&:capitalize).join
61
+ result = @name.to_s.gsub(/::/, '/')
62
+ result.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
63
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
64
+ .split('_')
65
+ .map(&:capitalize)
66
+ .join
62
67
  end
63
68
 
64
69
  # Generate the file