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,397 @@
|
|
|
1
|
+
module RailsForge
|
|
2
|
+
# ApiGenerator module handles generating API resources
|
|
3
|
+
module ApiGenerator
|
|
4
|
+
# Error class for invalid resource names
|
|
5
|
+
class InvalidResourceNameError < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Simple pluralize helper
|
|
8
|
+
def self.pluralize(word)
|
|
9
|
+
return word + 's' unless word.end_with?('s')
|
|
10
|
+
return word + 'es' if word.end_with?('sh') || word.end_with?('ch')
|
|
11
|
+
word + 's'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Simple underscore helper
|
|
15
|
+
def self.underscore(word)
|
|
16
|
+
word.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
17
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
18
|
+
.downcase
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Validates the resource name
|
|
22
|
+
def self.validate_resource_name(name)
|
|
23
|
+
if name.nil? || name.strip.empty?
|
|
24
|
+
raise InvalidResourceNameError, "Resource name cannot be empty"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
unless name =~ /\A[A-Z][a-zA-Z0-9]*\z/
|
|
28
|
+
raise InvalidResourceNameError, "Resource name must be in PascalCase (e.g., User)"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Generates an API resource with all necessary files
|
|
33
|
+
def self.generate(resource_name, with_spec: true, version: "v1", namespace: "api")
|
|
34
|
+
validate_resource_name(resource_name)
|
|
35
|
+
|
|
36
|
+
base_path = find_rails_app_path
|
|
37
|
+
raise InvalidResourceNameError, "Not in a Rails application directory" unless base_path
|
|
38
|
+
|
|
39
|
+
results = []
|
|
40
|
+
|
|
41
|
+
# Generate directories
|
|
42
|
+
controller_dir = File.join(base_path, "app", "controllers", namespace, version)
|
|
43
|
+
FileUtils.mkdir_p(controller_dir)
|
|
44
|
+
|
|
45
|
+
serializer_dir = File.join(base_path, "app", "serializers")
|
|
46
|
+
FileUtils.mkdir_p(serializer_dir)
|
|
47
|
+
|
|
48
|
+
policy_dir = File.join(base_path, "app", "policies")
|
|
49
|
+
FileUtils.mkdir_p(policy_dir)
|
|
50
|
+
|
|
51
|
+
service_dir = File.join(base_path, "app", "services")
|
|
52
|
+
FileUtils.mkdir_p(service_dir)
|
|
53
|
+
|
|
54
|
+
query_dir = File.join(base_path, "app", "queries")
|
|
55
|
+
FileUtils.mkdir_p(query_dir)
|
|
56
|
+
|
|
57
|
+
results << generate_controller(base_path, resource_name, namespace, version)
|
|
58
|
+
results << generate_serializer(base_path, resource_name)
|
|
59
|
+
results << generate_policy(base_path, resource_name)
|
|
60
|
+
results << generate_service(base_path, resource_name)
|
|
61
|
+
results << generate_query(base_path, resource_name)
|
|
62
|
+
|
|
63
|
+
if with_spec
|
|
64
|
+
spec_dir = File.join(base_path, "spec", "requests", namespace, version)
|
|
65
|
+
FileUtils.mkdir_p(spec_dir)
|
|
66
|
+
results << generate_request_spec(base_path, resource_name, namespace, version)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
"API resource '#{resource_name}' generated successfully with #{results.count} files!"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.generate_controller(base_path, resource_name, namespace, version)
|
|
73
|
+
resource_plural = pluralize(resource_name)
|
|
74
|
+
resource_underscore = underscore(resource_name)
|
|
75
|
+
resource_underscore_plural = pluralize(resource_underscore)
|
|
76
|
+
|
|
77
|
+
file_name = "#{resource_underscore}_controller.rb"
|
|
78
|
+
file_path = File.join(base_path, "app", "controllers", namespace, version, file_name)
|
|
79
|
+
|
|
80
|
+
return " Skipping controller (already exists)" if File.exist?(file_path)
|
|
81
|
+
|
|
82
|
+
content = <<~RUBY
|
|
83
|
+
# API Controller for #{resource_name}
|
|
84
|
+
# Version: #{version}
|
|
85
|
+
# Namespace: #{namespace}
|
|
86
|
+
#
|
|
87
|
+
# Generates standard CRUD actions for REST API
|
|
88
|
+
class #{resource_plural}Controller < ApplicationController
|
|
89
|
+
before_action :set_#{resource_underscore}, only: [:show, :update, :destroy]
|
|
90
|
+
before_action :authenticate_user!, unless: :devise_controller?
|
|
91
|
+
|
|
92
|
+
# GET /#{resource_underscore_plural}
|
|
93
|
+
def index
|
|
94
|
+
@#{resource_underscore_plural} = #{resource_name}Query.call
|
|
95
|
+
render json: #{resource_name}Serializer.new(@#{resource_underscore_plural}).serializable_hash
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# GET /#{resource_underscore_plural}/:id
|
|
99
|
+
def show
|
|
100
|
+
render json: #{resource_name}Serializer.new(@#{resource_underscore}).serializable_hash
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# POST /#{resource_underscore_plural}
|
|
104
|
+
def create
|
|
105
|
+
@result = Create#{resource_name}Service.call(#{resource_underscore}_params)
|
|
106
|
+
|
|
107
|
+
if @result.success?
|
|
108
|
+
render json: #{resource_name}Serializer.new(@result.#{resource_underscore}).serializable_hash, status: :created
|
|
109
|
+
else
|
|
110
|
+
render json: { errors: @result.errors }, status: :unprocessable_entity
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# PATCH/PUT /#{resource_underscore_plural}/:id
|
|
115
|
+
def update
|
|
116
|
+
@result = Update#{resource_name}Service.call(@#{resource_underscore}, #{resource_underscore}_params)
|
|
117
|
+
|
|
118
|
+
if @result.success?
|
|
119
|
+
render json: #{resource_name}Serializer.new(@result.#{resource_underscore}).serializable_hash
|
|
120
|
+
else
|
|
121
|
+
render json: { errors: @result.errors }, status: :unprocessable_entity
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# DELETE /#{resource_underscore_plural}/:id
|
|
126
|
+
def destroy
|
|
127
|
+
@#{resource_underscore}.destroy
|
|
128
|
+
head :no_content
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def set_#{resource_underscore}
|
|
134
|
+
@#{resource_underscore} = #{resource_name}.find(params[:id])
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def #{resource_underscore}_params
|
|
138
|
+
params.require(:#{resource_underscore}).permit(:name)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
RUBY
|
|
142
|
+
|
|
143
|
+
File.write(file_path, content)
|
|
144
|
+
puts " Created app/controllers/#{namespace}/#{version}/#{file_name}"
|
|
145
|
+
file_path
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def self.generate_serializer(base_path, resource_name)
|
|
149
|
+
file_name = "#{underscore(resource_name)}_serializer.rb"
|
|
150
|
+
file_path = File.join(base_path, "app", "serializers", file_name)
|
|
151
|
+
|
|
152
|
+
return " Skipping serializer (already exists)" if File.exist?(file_path)
|
|
153
|
+
|
|
154
|
+
content = <<~RUBY
|
|
155
|
+
# Serializer for #{resource_name}
|
|
156
|
+
class #{resource_name}Serializer < ApplicationSerializer
|
|
157
|
+
attributes :id, :name, :created_at, :updated_at
|
|
158
|
+
end
|
|
159
|
+
RUBY
|
|
160
|
+
|
|
161
|
+
File.write(file_path, content)
|
|
162
|
+
puts " Created app/serializers/#{file_name}"
|
|
163
|
+
file_path
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def self.generate_policy(base_path, resource_name)
|
|
167
|
+
file_name = "#{underscore(resource_name)}_policy.rb"
|
|
168
|
+
file_path = File.join(base_path, "app", "policies", file_name)
|
|
169
|
+
|
|
170
|
+
return " Skipping policy (already exists)" if File.exist?(file_path)
|
|
171
|
+
|
|
172
|
+
content = <<~RUBY
|
|
173
|
+
# Policy for #{resource_name}
|
|
174
|
+
class #{resource_name}Policy
|
|
175
|
+
attr_reader :user, :record
|
|
176
|
+
|
|
177
|
+
def initialize(user, record)
|
|
178
|
+
@user = user
|
|
179
|
+
@record = record
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def index?
|
|
183
|
+
true
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def show?
|
|
187
|
+
true
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def create?
|
|
191
|
+
user.present?
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def update?
|
|
195
|
+
user.present?
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def destroy?
|
|
199
|
+
user.present?
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
class Scope
|
|
203
|
+
def initialize(user, scope)
|
|
204
|
+
@user = user
|
|
205
|
+
@scope = scope
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def resolve
|
|
209
|
+
scope.all
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
RUBY
|
|
214
|
+
|
|
215
|
+
File.write(file_path, content)
|
|
216
|
+
puts " Created app/policies/#{file_name}"
|
|
217
|
+
file_path
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def self.generate_service(base_path, resource_name)
|
|
221
|
+
res_underscore = underscore(resource_name)
|
|
222
|
+
|
|
223
|
+
# Create service
|
|
224
|
+
file_name = "create_#{res_underscore}_service.rb"
|
|
225
|
+
file_path = File.join(base_path, "app", "services", file_name)
|
|
226
|
+
|
|
227
|
+
unless File.exist?(file_path)
|
|
228
|
+
content = <<~RUBY
|
|
229
|
+
# Service for creating #{resource_name}
|
|
230
|
+
class Create#{resource_name}Service
|
|
231
|
+
def initialize(params)
|
|
232
|
+
@params = params
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def call
|
|
236
|
+
@#{res_underscore} = #{resource_name}.create!(@params)
|
|
237
|
+
Result.new(success: true, #{res_underscore}: @#{res_underscore})
|
|
238
|
+
rescue => e
|
|
239
|
+
Result.new(success: false, errors: e.message)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
class Result
|
|
243
|
+
attr_reader :#{res_underscore}, :errors
|
|
244
|
+
def initialize(success:, #{res_underscore}: nil, errors: nil)
|
|
245
|
+
@success = success
|
|
246
|
+
@#{res_underscore} = #{res_underscore}
|
|
247
|
+
@errors = errors
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def success?
|
|
251
|
+
@success
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
RUBY
|
|
256
|
+
|
|
257
|
+
File.write(file_path, content)
|
|
258
|
+
puts " Created app/services/#{file_name}"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Update service
|
|
262
|
+
file_name = "update_#{res_underscore}_service.rb"
|
|
263
|
+
file_path = File.join(base_path, "app", "services", file_name)
|
|
264
|
+
|
|
265
|
+
unless File.exist?(file_path)
|
|
266
|
+
content = <<~RUBY
|
|
267
|
+
# Service for updating #{resource_name}
|
|
268
|
+
class Update#{resource_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
|
+
puts " Created app/services/#{file_name}"
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
"services"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def self.generate_query(base_path, resource_name)
|
|
305
|
+
file_name = "find_#{underscore(resource_name)}.rb"
|
|
306
|
+
file_path = File.join(base_path, "app", "queries", file_name)
|
|
307
|
+
|
|
308
|
+
return " Skipping query (already exists)" if File.exist?(file_path)
|
|
309
|
+
|
|
310
|
+
content = <<~RUBY
|
|
311
|
+
# Query for finding #{resource_name}
|
|
312
|
+
class Find#{resource_name}
|
|
313
|
+
def initialize(scope: nil)
|
|
314
|
+
@scope = scope || #{resource_name}.all
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def call
|
|
318
|
+
@scope
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
RUBY
|
|
322
|
+
|
|
323
|
+
File.write(file_path, content)
|
|
324
|
+
puts " Created app/queries/#{file_name}"
|
|
325
|
+
file_path
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def self.generate_request_spec(base_path, resource_name, namespace, version)
|
|
329
|
+
resource_plural = pluralize(resource_name)
|
|
330
|
+
resource_underscore = underscore(resource_name)
|
|
331
|
+
|
|
332
|
+
file_name = "#{resource_underscore}_spec.rb"
|
|
333
|
+
file_path = File.join(base_path, "spec", "requests", namespace, version, file_name)
|
|
334
|
+
|
|
335
|
+
return " Skipping spec (already exists)" if File.exist?(file_path)
|
|
336
|
+
|
|
337
|
+
content = <<~RUBY
|
|
338
|
+
require 'rails_helper'
|
|
339
|
+
|
|
340
|
+
RSpec.describe "#{namespace.capitalize}::#{version.capitalize}::#{resource_plural}Controller" do
|
|
341
|
+
let(:user) { User.create!(name: "Test", email: "test@example.com") }
|
|
342
|
+
let(:#{resource_underscore}) { #{resource_name}.create!(name: "Test #{resource_name}") }
|
|
343
|
+
|
|
344
|
+
before do
|
|
345
|
+
sign_in user
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
describe "GET /index" do
|
|
349
|
+
it "returns a success response" do
|
|
350
|
+
get :index
|
|
351
|
+
expect(response).to be_successful
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
describe "GET /show" do
|
|
356
|
+
it "returns a success response" do
|
|
357
|
+
get :show, params: { id: #{resource_underscore}.id }
|
|
358
|
+
expect(response).to be_successful
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
describe "POST /create" do
|
|
363
|
+
it "creates a new #{resource_underscore}" do
|
|
364
|
+
expect {
|
|
365
|
+
post :create, params: { #{resource_underscore}: { name: "New #{resource_name}" } }
|
|
366
|
+
}.to change(#{resource_name}, :count).by(1)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
describe "DELETE /destroy" do
|
|
371
|
+
it "deletes the #{resource_underscore}" do
|
|
372
|
+
#{resource_underscore}
|
|
373
|
+
expect {
|
|
374
|
+
delete :destroy, params: { id: #{resource_underscore}.id }
|
|
375
|
+
}.to change(#{resource_name}, :count).by(-1)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
RUBY
|
|
380
|
+
|
|
381
|
+
File.write(file_path, content)
|
|
382
|
+
puts " Created spec/requests/#{namespace}/#{version}/#{file_name}"
|
|
383
|
+
file_path
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def self.find_rails_app_path
|
|
387
|
+
path = Dir.pwd
|
|
388
|
+
10.times do
|
|
389
|
+
return path if File.exist?(File.join(path, "config", "application.rb"))
|
|
390
|
+
parent = File.dirname(path)
|
|
391
|
+
break if parent == path
|
|
392
|
+
path = parent
|
|
393
|
+
end
|
|
394
|
+
nil
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Audit module for RailsForge
|
|
2
|
+
# Runs a comprehensive architecture health check
|
|
3
|
+
|
|
4
|
+
module RailsForge
|
|
5
|
+
# Audit module combines all analyzers to produce a comprehensive report
|
|
6
|
+
module Audit
|
|
7
|
+
# Error class for audit issues
|
|
8
|
+
class AuditError < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Default scoring weights for each analyzer
|
|
11
|
+
WEIGHTS = {
|
|
12
|
+
controllers: 25,
|
|
13
|
+
models: 25,
|
|
14
|
+
database: 20,
|
|
15
|
+
specs: 15,
|
|
16
|
+
metrics: 15
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
# Run full audit and return results
|
|
20
|
+
# @param base_path [String] Rails app root path
|
|
21
|
+
# @return [Hash] Complete audit results
|
|
22
|
+
def self.run(base_path = nil)
|
|
23
|
+
base_path ||= find_rails_app_path
|
|
24
|
+
raise AuditError, "Not in a Rails application directory" unless base_path
|
|
25
|
+
|
|
26
|
+
results = {
|
|
27
|
+
timestamp: Time.now.iso8601,
|
|
28
|
+
base_path: base_path,
|
|
29
|
+
controllers: run_controller_analysis(base_path),
|
|
30
|
+
models: run_model_analysis(base_path),
|
|
31
|
+
database: run_database_analysis(base_path),
|
|
32
|
+
specs: run_spec_analysis(base_path),
|
|
33
|
+
metrics: run_metrics_analysis(base_path)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
results[:score] = calculate_score(results)
|
|
37
|
+
results
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Run controller analysis
|
|
41
|
+
# @param base_path [String] Rails app root path
|
|
42
|
+
# @return [Hash] Controller analysis results
|
|
43
|
+
def self.run_controller_analysis(base_path)
|
|
44
|
+
begin
|
|
45
|
+
issues = ControllerAnalyzer.analyze(base_path)
|
|
46
|
+
{
|
|
47
|
+
issues_count: issues.length,
|
|
48
|
+
issues: issues,
|
|
49
|
+
status: issues.empty? ? :pass : :warning
|
|
50
|
+
}
|
|
51
|
+
rescue => e
|
|
52
|
+
{ issues_count: 0, issues: [], status: :error, error: e.message }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Run model analysis
|
|
57
|
+
# @param base_path [String] Rails app root path
|
|
58
|
+
# @return [Hash] Model analysis results
|
|
59
|
+
def self.run_model_analysis(base_path)
|
|
60
|
+
begin
|
|
61
|
+
issues = ModelAnalyzer.analyze(base_path)
|
|
62
|
+
{
|
|
63
|
+
issues_count: issues.length,
|
|
64
|
+
issues: issues,
|
|
65
|
+
status: issues.empty? ? :pass : :warning
|
|
66
|
+
}
|
|
67
|
+
rescue => e
|
|
68
|
+
{ issues_count: 0, issues: [], status: :error, error: e.message }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Run database analysis
|
|
73
|
+
# @param base_path [String] Rails app root path
|
|
74
|
+
# @return [Hash] Database analysis results
|
|
75
|
+
def self.run_database_analysis(base_path)
|
|
76
|
+
begin
|
|
77
|
+
issues = DatabaseAnalyzer.analyze(base_path)
|
|
78
|
+
{
|
|
79
|
+
issues_count: issues.length,
|
|
80
|
+
issues: issues,
|
|
81
|
+
status: issues.empty? ? :pass : :warning
|
|
82
|
+
}
|
|
83
|
+
rescue => e
|
|
84
|
+
{ issues_count: 0, issues: [], status: :error, error: e.message }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Run spec analysis
|
|
89
|
+
# @param base_path [String] Rails app root path
|
|
90
|
+
# @return [Hash] Spec analysis results
|
|
91
|
+
def self.run_spec_analysis(base_path)
|
|
92
|
+
begin
|
|
93
|
+
results = SpecAnalyzer.analyze(base_path)
|
|
94
|
+
missing_count = results.count { |r| r[:has_spec] == false }
|
|
95
|
+
{
|
|
96
|
+
issues_count: missing_count,
|
|
97
|
+
coverage: results,
|
|
98
|
+
status: missing_count == 0 ? :pass : :warning
|
|
99
|
+
}
|
|
100
|
+
rescue => e
|
|
101
|
+
{ issues_count: 0, coverage: [], status: :error, error: e.message }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Run metrics analysis
|
|
106
|
+
# @param base_path [String] Rails app root path
|
|
107
|
+
# @return [Hash] Metrics analysis results
|
|
108
|
+
def self.run_metrics_analysis(base_path)
|
|
109
|
+
begin
|
|
110
|
+
metrics = MetricsAnalyzer.analyze(base_path)
|
|
111
|
+
{
|
|
112
|
+
metrics: metrics,
|
|
113
|
+
status: :pass
|
|
114
|
+
}
|
|
115
|
+
rescue => e
|
|
116
|
+
{ metrics: {}, status: :error, error: e.message }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Calculate overall architecture score
|
|
121
|
+
# @param results [Hash] All analysis results
|
|
122
|
+
# @return [Integer] Score out of 100
|
|
123
|
+
def self.calculate_score(results)
|
|
124
|
+
score = 100
|
|
125
|
+
|
|
126
|
+
# Deduct points for controller issues
|
|
127
|
+
controller_issues = results[:controllers][:issues_count]
|
|
128
|
+
score -= [controller_issues * 5, WEIGHTS[:controllers]].min
|
|
129
|
+
|
|
130
|
+
# Deduct points for model issues
|
|
131
|
+
model_issues = results[:models][:issues_count]
|
|
132
|
+
score -= [model_issues * 5, WEIGHTS[:models]].min
|
|
133
|
+
|
|
134
|
+
# Deduct points for database issues
|
|
135
|
+
db_issues = results[:database][:issues_count]
|
|
136
|
+
score -= [db_issues * 3, WEIGHTS[:database]].min
|
|
137
|
+
|
|
138
|
+
# Deduct points for missing specs
|
|
139
|
+
spec_issues = results[:specs][:issues_count]
|
|
140
|
+
score -= [spec_issues * 2, WEIGHTS[:specs]].min
|
|
141
|
+
|
|
142
|
+
[score, 0].max.to_i
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Format results as a readable report
|
|
146
|
+
# @param results [Hash] Audit results
|
|
147
|
+
# @return [String] Formatted report
|
|
148
|
+
def self.format_report(results)
|
|
149
|
+
score = results[:score]
|
|
150
|
+
score_color = score >= 80 ? :green : score >= 50 ? :yellow : :red
|
|
151
|
+
|
|
152
|
+
output = []
|
|
153
|
+
output << "=" * 60
|
|
154
|
+
output << "RailsForge Architecture Audit"
|
|
155
|
+
output << "=" * 60
|
|
156
|
+
output << ""
|
|
157
|
+
output << "Architecture Score: #{colorize(score, score_color)}/100"
|
|
158
|
+
output << ""
|
|
159
|
+
|
|
160
|
+
# Controllers section
|
|
161
|
+
output << "-" * 40
|
|
162
|
+
output << "Controllers:"
|
|
163
|
+
controller_issues = results[:controllers][:issues_count]
|
|
164
|
+
if controller_issues == 0
|
|
165
|
+
output << " ✓ No issues found (#{controller_issues})"
|
|
166
|
+
else
|
|
167
|
+
output << " ⚠ #{controller_issues} issue(s) found"
|
|
168
|
+
results[:controllers][:issues].first(5).each do |issue|
|
|
169
|
+
output << " - #{issue[:file]}: #{issue[:issues].join(', ')}"
|
|
170
|
+
end
|
|
171
|
+
output << " ..." if results[:controllers][:issues].count > 5
|
|
172
|
+
end
|
|
173
|
+
output << ""
|
|
174
|
+
|
|
175
|
+
# Models section
|
|
176
|
+
output << "-" * 40
|
|
177
|
+
output << "Models:"
|
|
178
|
+
model_issues = results[:models][:issues_count]
|
|
179
|
+
if model_issues == 0
|
|
180
|
+
output << " ✓ No issues found (#{model_issues})"
|
|
181
|
+
else
|
|
182
|
+
output << " ⚠ #{model_issues} issue(s) found"
|
|
183
|
+
results[:models][:issues].first(5).each do |issue|
|
|
184
|
+
output << " - #{issue[:file]}: #{issue[:issues].join(', ')}"
|
|
185
|
+
end
|
|
186
|
+
output << " ..." if results[:models][:issues].count > 5
|
|
187
|
+
end
|
|
188
|
+
output << ""
|
|
189
|
+
|
|
190
|
+
# Database section
|
|
191
|
+
output << "-" * 40
|
|
192
|
+
output << "Database:"
|
|
193
|
+
db_issues = results[:database][:issues_count]
|
|
194
|
+
if db_issues == 0
|
|
195
|
+
output << " ✓ No issues found (#{db_issues})"
|
|
196
|
+
else
|
|
197
|
+
output << " ⚠ #{db_issues} issue(s) found"
|
|
198
|
+
results[:database][:issues].first(5).each do |issue|
|
|
199
|
+
output << " - #{issue[:table]}: #{issue[:issue]}"
|
|
200
|
+
end
|
|
201
|
+
output << " ..." if results[:database][:issues].count > 5
|
|
202
|
+
end
|
|
203
|
+
output << ""
|
|
204
|
+
|
|
205
|
+
# Specs section
|
|
206
|
+
output << "-" * 40
|
|
207
|
+
output << "Specs:"
|
|
208
|
+
spec_issues = results[:specs][:issues_count]
|
|
209
|
+
if spec_issues == 0
|
|
210
|
+
output << " ✓ All components have specs (#{spec_issues} missing)"
|
|
211
|
+
else
|
|
212
|
+
output << " ⚠ #{spec_issues} component(s) missing specs"
|
|
213
|
+
results[:specs][:coverage].select { |r| r[:has_spec] == false }.first(5).each do |item|
|
|
214
|
+
output << " - #{item[:type]}: #{item[:name]}"
|
|
215
|
+
end
|
|
216
|
+
output << " ..." if results[:specs][:coverage].count { |r| r[:has_spec] == false } > 5
|
|
217
|
+
end
|
|
218
|
+
output << ""
|
|
219
|
+
|
|
220
|
+
# Metrics section
|
|
221
|
+
output << "-" * 40
|
|
222
|
+
output << "Metrics:"
|
|
223
|
+
metrics = results[:metrics][:metrics]
|
|
224
|
+
if metrics && !metrics.empty?
|
|
225
|
+
output << " Files: #{metrics[:total_files] || 'N/A'}"
|
|
226
|
+
output << " Lines: #{metrics[:total_lines] || 'N/A'}"
|
|
227
|
+
output << " Services: #{metrics[:services_count] || 0}"
|
|
228
|
+
output << " Queries: #{metrics[:queries_count] || 0}"
|
|
229
|
+
output << " Jobs: #{metrics[:jobs_count] || 0}"
|
|
230
|
+
else
|
|
231
|
+
output << " No metrics available"
|
|
232
|
+
end
|
|
233
|
+
output << ""
|
|
234
|
+
|
|
235
|
+
output << "=" * 60
|
|
236
|
+
output << "Audit completed at #{results[:timestamp]}"
|
|
237
|
+
output << "=" * 60
|
|
238
|
+
|
|
239
|
+
output.join("\n")
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Print results to console
|
|
243
|
+
# @param results [Hash] Audit results
|
|
244
|
+
def self.print_report(results)
|
|
245
|
+
puts format_report(results)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Convert results to JSON
|
|
249
|
+
# @param results [Hash] Audit results
|
|
250
|
+
# @return [String] JSON string
|
|
251
|
+
def self.to_json(results)
|
|
252
|
+
require 'json'
|
|
253
|
+
# Remove non-serializable data
|
|
254
|
+
serializable = results.dup
|
|
255
|
+
serializable.delete(:timestamp)
|
|
256
|
+
JSON.pretty_generate(serializable)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Find Rails app path
|
|
260
|
+
# @return [String, nil] Rails app root path
|
|
261
|
+
def self.find_rails_app_path
|
|
262
|
+
path = Dir.pwd
|
|
263
|
+
max_depth = 10
|
|
264
|
+
|
|
265
|
+
max_depth.times do
|
|
266
|
+
return path if File.exist?(File.join(path, "config", "application.rb"))
|
|
267
|
+
parent = File.dirname(path)
|
|
268
|
+
break if parent == path
|
|
269
|
+
path = parent
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
nil
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Colorize output for terminal
|
|
276
|
+
# @param text [String] Text to colorize
|
|
277
|
+
# @param color [Symbol] Color name
|
|
278
|
+
# @return [String] Colorized text
|
|
279
|
+
def self.colorize(text, color)
|
|
280
|
+
colors = {
|
|
281
|
+
green: "\e[32m",
|
|
282
|
+
yellow: "\e[33m",
|
|
283
|
+
red: "\e[31m",
|
|
284
|
+
reset: "\e[0m"
|
|
285
|
+
}
|
|
286
|
+
"#{colors[color]}#{text}#{colors[:reset]}"
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|