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,287 @@
|
|
|
1
|
+
# DevOps generator for RailsForge
|
|
2
|
+
# Generates Dockerfile, docker-compose, and CI/CD configs
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Generators
|
|
8
|
+
# DevOps generator
|
|
9
|
+
class DevopsGenerator < BaseGenerator
|
|
10
|
+
# Initialize the generator
|
|
11
|
+
def initialize(name = "app", options = {})
|
|
12
|
+
super(name, options)
|
|
13
|
+
@docker_image = options[:docker_image] || "railsforge/#{name}"
|
|
14
|
+
@ci_provider = options[:ci_provider] || "github"
|
|
15
|
+
@ruby_version = options[:ruby_version] || "3.2.0"
|
|
16
|
+
@node_version = options[:node_version] || "18"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Generate DevOps files
|
|
20
|
+
def generate
|
|
21
|
+
return "Not in a Rails application directory" unless @base_path
|
|
22
|
+
|
|
23
|
+
results = []
|
|
24
|
+
results << create_dockerfile
|
|
25
|
+
results << create_docker_compose
|
|
26
|
+
results << create_dockerignore
|
|
27
|
+
results << create_github_workflow if @ci_provider == "github"
|
|
28
|
+
results << create_gitlab_ci if @ci_provider == "gitlab"
|
|
29
|
+
|
|
30
|
+
results.join("\n")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Create Dockerfile
|
|
36
|
+
def create_dockerfile
|
|
37
|
+
dockerfile_path = File.join(@base_path, "Dockerfile")
|
|
38
|
+
return "Skipping Dockerfile (already exists)" if File.exist?(dockerfile_path)
|
|
39
|
+
|
|
40
|
+
content = <<~DOCKERFILE
|
|
41
|
+
# Ruby version
|
|
42
|
+
FROM ruby:#{@ruby_version}-slim
|
|
43
|
+
|
|
44
|
+
# Install system dependencies
|
|
45
|
+
RUN apt-get update && apt-get install -y \\
|
|
46
|
+
build-essential \\
|
|
47
|
+
libpq-dev \\
|
|
48
|
+
curl \\
|
|
49
|
+
git \\
|
|
50
|
+
nodejs \\
|
|
51
|
+
npm \\
|
|
52
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
53
|
+
|
|
54
|
+
# Set working directory
|
|
55
|
+
WORKDIR /app
|
|
56
|
+
|
|
57
|
+
# Copy Gemfile
|
|
58
|
+
COPY Gemfile Gemfile.lock ./
|
|
59
|
+
|
|
60
|
+
# Install gems
|
|
61
|
+
RUN bundle install --jobs 4 --retry 3
|
|
62
|
+
|
|
63
|
+
# Copy package.json for assets
|
|
64
|
+
COPY package.json package-lock.json ./
|
|
65
|
+
RUN npm install --production
|
|
66
|
+
|
|
67
|
+
# Copy application code
|
|
68
|
+
COPY . .
|
|
69
|
+
|
|
70
|
+
# Precompile assets
|
|
71
|
+
RUN RAILS_ENV=production SECRET_KEY_BASE=dummy bundle exec rails assets:precompile
|
|
72
|
+
|
|
73
|
+
# Expose port
|
|
74
|
+
EXPOSE 3000
|
|
75
|
+
|
|
76
|
+
# Start server
|
|
77
|
+
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
|
|
78
|
+
DOCKERFILE
|
|
79
|
+
|
|
80
|
+
File.write(dockerfile_path, content)
|
|
81
|
+
"Created Dockerfile"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Create docker-compose.yml
|
|
85
|
+
def create_docker_compose
|
|
86
|
+
compose_path = File.join(@base_path, "docker-compose.yml")
|
|
87
|
+
return "Skipping docker-compose.yml (already exists)" if File.exist?(compose_path)
|
|
88
|
+
|
|
89
|
+
content = <<~YAML
|
|
90
|
+
version: '3.8'
|
|
91
|
+
|
|
92
|
+
services:
|
|
93
|
+
app:
|
|
94
|
+
build: .
|
|
95
|
+
ports:
|
|
96
|
+
- "3000:3000"
|
|
97
|
+
environment:
|
|
98
|
+
- RAILS_ENV=development
|
|
99
|
+
- DATABASE_HOST=postgres
|
|
100
|
+
- REDIS_HOST=redis
|
|
101
|
+
depends_on:
|
|
102
|
+
- postgres
|
|
103
|
+
- redis
|
|
104
|
+
volumes:
|
|
105
|
+
- .:/app
|
|
106
|
+
|
|
107
|
+
postgres:
|
|
108
|
+
image: postgres:15-alpine
|
|
109
|
+
environment:
|
|
110
|
+
- POSTGRES_USER=rails
|
|
111
|
+
- POSTGRES_PASSWORD=password
|
|
112
|
+
- POSTGRES_DB=app_development
|
|
113
|
+
ports:
|
|
114
|
+
- "5432:5432"
|
|
115
|
+
volumes:
|
|
116
|
+
- postgres_data:/var/lib/postgresql/data
|
|
117
|
+
|
|
118
|
+
redis:
|
|
119
|
+
image: redis:7-alpine
|
|
120
|
+
ports:
|
|
121
|
+
- "6379:6379"
|
|
122
|
+
|
|
123
|
+
sidekiq:
|
|
124
|
+
build: .
|
|
125
|
+
command: bundle exec sidekiq
|
|
126
|
+
environment:
|
|
127
|
+
- RAILS_ENV=development
|
|
128
|
+
- DATABASE_HOST=postgres
|
|
129
|
+
- REDIS_HOST=redis
|
|
130
|
+
depends_on:
|
|
131
|
+
- postgres
|
|
132
|
+
- redis
|
|
133
|
+
volumes:
|
|
134
|
+
- .:/app
|
|
135
|
+
|
|
136
|
+
volumes:
|
|
137
|
+
postgres_data:
|
|
138
|
+
YAML
|
|
139
|
+
|
|
140
|
+
File.write(compose_path, content)
|
|
141
|
+
"Created docker-compose.yml"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Create .dockerignore
|
|
145
|
+
def create_dockerignore
|
|
146
|
+
ignore_path = File.join(@base_path, ".dockerignore")
|
|
147
|
+
return "Skipping .dockerignore (already exists)" if File.exist?(ignore_path)
|
|
148
|
+
|
|
149
|
+
content = <<~IGNORE
|
|
150
|
+
.git
|
|
151
|
+
.github
|
|
152
|
+
.gitignore
|
|
153
|
+
log/*.log
|
|
154
|
+
tmp/
|
|
155
|
+
.bundle/
|
|
156
|
+
node_modules/
|
|
157
|
+
public/assets/
|
|
158
|
+
.DS_Store
|
|
159
|
+
*.swp
|
|
160
|
+
*.swo
|
|
161
|
+
IGNORE
|
|
162
|
+
|
|
163
|
+
File.write(ignore_path, content)
|
|
164
|
+
"Created .dockerignore"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Create GitHub Actions workflow
|
|
168
|
+
def create_github_workflow
|
|
169
|
+
workflow_dir = File.join(@base_path, ".github", "workflows")
|
|
170
|
+
FileUtils.mkdir_p(workflow_dir)
|
|
171
|
+
|
|
172
|
+
workflow_path = File.join(workflow_dir, "ci.yml")
|
|
173
|
+
return "Skipping GitHub workflow (already exists)" if File.exist?(workflow_path)
|
|
174
|
+
|
|
175
|
+
content = <<~YAML
|
|
176
|
+
name: CI
|
|
177
|
+
|
|
178
|
+
on:
|
|
179
|
+
push:
|
|
180
|
+
branches: [main, develop]
|
|
181
|
+
pull_request:
|
|
182
|
+
branches: [main, develop]
|
|
183
|
+
|
|
184
|
+
jobs:
|
|
185
|
+
test:
|
|
186
|
+
runs-on: ubuntu-latest
|
|
187
|
+
services:
|
|
188
|
+
postgres:
|
|
189
|
+
image: postgres:15-alpine
|
|
190
|
+
env:
|
|
191
|
+
POSTGRES_USER: rails
|
|
192
|
+
POSTGRES_PASSWORD: password
|
|
193
|
+
POSTGRES_DB: app_test
|
|
194
|
+
ports:
|
|
195
|
+
- 5432:5432
|
|
196
|
+
redis:
|
|
197
|
+
image: redis:7-alpine
|
|
198
|
+
ports:
|
|
199
|
+
- 6379:6379
|
|
200
|
+
|
|
201
|
+
steps:
|
|
202
|
+
- uses: actions/checkout@v3
|
|
203
|
+
- name: Set up Ruby
|
|
204
|
+
uses: ruby/setup-ruby@v1
|
|
205
|
+
with:
|
|
206
|
+
ruby-version: #{@ruby_version}
|
|
207
|
+
bundler-cache: true
|
|
208
|
+
- name: Install dependencies
|
|
209
|
+
run: |
|
|
210
|
+
bundle install
|
|
211
|
+
npm install
|
|
212
|
+
- name: Setup database
|
|
213
|
+
env:
|
|
214
|
+
RAILS_ENV: test
|
|
215
|
+
POSTGRES_HOST: localhost
|
|
216
|
+
POSTGRES_USER: rails
|
|
217
|
+
POSTGRES_PASSWORD: password
|
|
218
|
+
POSTGRES_DB: app_test
|
|
219
|
+
run: bundle exec rails db:create db:schema:load
|
|
220
|
+
- name: Run tests
|
|
221
|
+
env:
|
|
222
|
+
RAILS_ENV: test
|
|
223
|
+
POSTGRES_HOST: localhost
|
|
224
|
+
POSTGRES_USER: rails
|
|
225
|
+
POSTGRES_PASSWORD: password
|
|
226
|
+
POSTGRES_DB: app_test
|
|
227
|
+
run: bundle exec rspec
|
|
228
|
+
- name: Run linter
|
|
229
|
+
run: bundle exec rubocop
|
|
230
|
+
YAML
|
|
231
|
+
|
|
232
|
+
File.write(workflow_path, content)
|
|
233
|
+
"Created GitHub Actions workflow"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Create GitLab CI config
|
|
237
|
+
def create_gitlab_ci
|
|
238
|
+
ci_path = File.join(@base_path, ".gitlab-ci.yml")
|
|
239
|
+
return "Skipping .gitlab-ci.yml (already exists)" if File.exist?(ci_path)
|
|
240
|
+
|
|
241
|
+
content = <<~YAML
|
|
242
|
+
stages:
|
|
243
|
+
- test
|
|
244
|
+
- lint
|
|
245
|
+
- deploy
|
|
246
|
+
|
|
247
|
+
variables:
|
|
248
|
+
RUBY_VERSION: #{@ruby_version}
|
|
249
|
+
POSTGRES_DB: app_test
|
|
250
|
+
POSTGRES_USER: rails
|
|
251
|
+
POSTGRES_PASSWORD: password
|
|
252
|
+
|
|
253
|
+
test:
|
|
254
|
+
stage: test
|
|
255
|
+
image: ruby:#{@ruby_version}
|
|
256
|
+
services:
|
|
257
|
+
- postgres:15-alpine
|
|
258
|
+
- redis:7-alpine
|
|
259
|
+
before_script:
|
|
260
|
+
- apt-get update -qq && apt-get install -y -qq nodejs npm
|
|
261
|
+
- bundle install
|
|
262
|
+
- npm install
|
|
263
|
+
script:
|
|
264
|
+
- RAILS_ENV=test bundle exec rails db:create db:schema:load
|
|
265
|
+
- bundle exec rspec
|
|
266
|
+
only:
|
|
267
|
+
- main
|
|
268
|
+
- develop
|
|
269
|
+
|
|
270
|
+
rubocop:
|
|
271
|
+
stage: lint
|
|
272
|
+
image: ruby:#{@ruby_version}
|
|
273
|
+
before_script:
|
|
274
|
+
- bundle install
|
|
275
|
+
script:
|
|
276
|
+
- bundle exec rubocop
|
|
277
|
+
only:
|
|
278
|
+
- main
|
|
279
|
+
- develop
|
|
280
|
+
YAML
|
|
281
|
+
|
|
282
|
+
File.write(ci_path, content)
|
|
283
|
+
"Created GitLab CI config"
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Monitoring generator for RailsForge
|
|
2
|
+
# Generates Sentry and Lograge configurations
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Generators
|
|
8
|
+
# Monitoring generator
|
|
9
|
+
class MonitoringGenerator < BaseGenerator
|
|
10
|
+
# Initialize the generator
|
|
11
|
+
def initialize(name = "monitoring", options = {})
|
|
12
|
+
super(name, options)
|
|
13
|
+
@sentry_dsn = options[:sentry_dsn] || ""
|
|
14
|
+
@environment = options[:environment] || (defined?(Rails) && Rails.env) || 'development'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Generate monitoring configs
|
|
18
|
+
def generate
|
|
19
|
+
return "Not in a Rails application directory" unless @base_path
|
|
20
|
+
|
|
21
|
+
results = []
|
|
22
|
+
results << create_sentry_initializer
|
|
23
|
+
results << create_lograge_config
|
|
24
|
+
results << create_log_formatter
|
|
25
|
+
results << create_environments_config
|
|
26
|
+
|
|
27
|
+
results.join("\n")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Create Sentry initializer
|
|
33
|
+
def create_sentry_initializer
|
|
34
|
+
init_dir = File.join(@base_path, "config", "initializers")
|
|
35
|
+
FileUtils.mkdir_p(init_dir)
|
|
36
|
+
|
|
37
|
+
sentry_path = File.join(init_dir, "sentry.rb")
|
|
38
|
+
return "Skipping Sentry initializer (already exists)" if File.exist?(sentry_path)
|
|
39
|
+
|
|
40
|
+
content = <<~RUBY
|
|
41
|
+
# Sentry error tracking configuration
|
|
42
|
+
# Install sentry-ruby and add to Gemfile: gem 'sentry-ruby', '~> 4.0'
|
|
43
|
+
Sentry.init do |config|
|
|
44
|
+
config.dsn = ENV.fetch('SENTRY_DSN', '#{@sentry_dsn}')
|
|
45
|
+
config.breadcrumb_logger = :active_support
|
|
46
|
+
config.environments = %w[staging production]
|
|
47
|
+
config.traces_sample_rate = 0.1
|
|
48
|
+
config.before_send = lambda do |event, hint|
|
|
49
|
+
# Filter out specific errors
|
|
50
|
+
if hint[:exception]
|
|
51
|
+
# Add custom filtering logic here
|
|
52
|
+
end
|
|
53
|
+
event
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
RUBY
|
|
57
|
+
|
|
58
|
+
File.write(sentry_path, content)
|
|
59
|
+
"Created config/initializers/sentry.rb"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Create Lograge configuration
|
|
63
|
+
def create_lograge_config
|
|
64
|
+
init_dir = File.join(@base_path, "config", "initializers")
|
|
65
|
+
FileUtils.mkdir_p(init_dir)
|
|
66
|
+
|
|
67
|
+
lograge_path = File.join(init_dir, "lograge.rb")
|
|
68
|
+
return "Skipping Lograge initializer (already exists)" if File.exist?(lograge_path)
|
|
69
|
+
|
|
70
|
+
content = <<~RUBY
|
|
71
|
+
# Lograge configuration for structured logging
|
|
72
|
+
# Add to Gemfile: gem 'lograge'
|
|
73
|
+
Rails.application.configure do
|
|
74
|
+
config.lograge.enabled = true
|
|
75
|
+
config.lograge.base_controller_class = 'ActionController::API'
|
|
76
|
+
config.lograge.custom_options = lambda do |event|
|
|
77
|
+
{
|
|
78
|
+
# Add custom data to logs
|
|
79
|
+
host: Socket.gethostname,
|
|
80
|
+
pid: Process.pid,
|
|
81
|
+
# Add timing data
|
|
82
|
+
request_id: event.payload[:request_id],
|
|
83
|
+
user_id: event.payload[:user_id]
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
config.lograge.formatter = Lograge::Formatters::Json.new
|
|
87
|
+
end
|
|
88
|
+
RUBY
|
|
89
|
+
|
|
90
|
+
File.write(lograge_path, content)
|
|
91
|
+
"Created config/initializers/lograge.rb"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Create log formatter
|
|
95
|
+
def create_log_formatter
|
|
96
|
+
config_path = File.join(@base_path, "config")
|
|
97
|
+
|
|
98
|
+
# Check if application.rb exists
|
|
99
|
+
app_rb = File.join(config_path, "application.rb")
|
|
100
|
+
return "Not a Rails app" unless File.exist?(app_rb)
|
|
101
|
+
|
|
102
|
+
content = File.read(app_rb)
|
|
103
|
+
|
|
104
|
+
unless content.include?("config.log_formatter")
|
|
105
|
+
# Add log formatter to application.rb
|
|
106
|
+
File.write(app_rb, content.gsub(/class Application < Rails::Application/,
|
|
107
|
+
"class Application < Rails::Application\n config.log_formatter = proc { |severity, datetime, progname, msg| \"#{datetime.utc_iso8601} #{severity}: #{msg}\\n\" }"))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
"Updated application.rb with custom log formatter"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Create environment-specific configs
|
|
114
|
+
def create_environments_config
|
|
115
|
+
env_dir = File.join(@base_path, "config", "environments")
|
|
116
|
+
return "Not a Rails app" unless Dir.exist?(env_dir)
|
|
117
|
+
|
|
118
|
+
results = []
|
|
119
|
+
|
|
120
|
+
# Production config
|
|
121
|
+
prod_path = File.join(env_dir, "production.rb")
|
|
122
|
+
if File.exist?(prod_path)
|
|
123
|
+
content = File.read(prod_path)
|
|
124
|
+
unless content.include?("Lograge") || content.include?("Sentry")
|
|
125
|
+
File.write(prod_path, content + "\n\n# Monitoring\nconfig.lograge.enabled = true\n")
|
|
126
|
+
results << "Updated production.rb"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
results.any? ? results.join("\n") : "No environment updates needed"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Service generator for RailsForge
|
|
2
|
+
# Generates service objects
|
|
3
|
+
|
|
4
|
+
require_relative 'base_generator'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Generators
|
|
8
|
+
# ServiceGenerator creates service objects
|
|
9
|
+
class ServiceGenerator < BaseGenerator
|
|
10
|
+
# Error class for invalid service names
|
|
11
|
+
class InvalidServiceNameError < StandardError; end
|
|
12
|
+
|
|
13
|
+
# Validates the service name
|
|
14
|
+
# @param name [String] Service name to validate
|
|
15
|
+
def validate_name!(name)
|
|
16
|
+
super(name, /\A[A-Z][a-zA-Z0-9]*\z/, InvalidServiceNameError)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Generate service file and optionally spec
|
|
20
|
+
# @return [String] Success message
|
|
21
|
+
def generate
|
|
22
|
+
validate_name!(@name)
|
|
23
|
+
|
|
24
|
+
unless @base_path
|
|
25
|
+
raise "Not in a Rails application directory"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
service_file_path = generate_service_file
|
|
29
|
+
|
|
30
|
+
if @options[:with_spec] != false
|
|
31
|
+
generate_spec_file
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
"Service '#{@name}' generated successfully!"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Generate the service file
|
|
38
|
+
# @return [String] Path to generated file
|
|
39
|
+
def generate_service_file
|
|
40
|
+
service_dir = File.join(@base_path, "app", "services")
|
|
41
|
+
FileUtils.mkdir_p(service_dir)
|
|
42
|
+
|
|
43
|
+
file_name = "#{underscore}.rb"
|
|
44
|
+
file_path = File.join(service_dir, file_name)
|
|
45
|
+
|
|
46
|
+
if File.exist?(file_path)
|
|
47
|
+
puts " Skipping service (already exists)"
|
|
48
|
+
return file_path
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
File.write(file_path, service_template)
|
|
52
|
+
puts " Created app/services/#{file_name}"
|
|
53
|
+
file_path
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Generate the spec file
|
|
57
|
+
# @return [String] Path to generated spec
|
|
58
|
+
def generate_spec_file
|
|
59
|
+
spec_dir = File.join(@base_path, "spec", "services")
|
|
60
|
+
FileUtils.mkdir_p(spec_dir)
|
|
61
|
+
|
|
62
|
+
file_name = "#{underscore}_spec.rb"
|
|
63
|
+
file_path = File.join(spec_dir, file_name)
|
|
64
|
+
|
|
65
|
+
if File.exist?(file_path)
|
|
66
|
+
puts " Skipping spec (already exists)"
|
|
67
|
+
return file_path
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
File.write(file_path, spec_template)
|
|
71
|
+
puts " Created spec/services/#{file_name}"
|
|
72
|
+
file_path
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Service template
|
|
76
|
+
# @return [String] Template content
|
|
77
|
+
def service_template
|
|
78
|
+
<<~RUBY
|
|
79
|
+
# Service class for #{underscore}
|
|
80
|
+
# Encapsulates business logic related to #{underscore}
|
|
81
|
+
class #{@name}
|
|
82
|
+
def initialize(**args)
|
|
83
|
+
@args = args
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def call
|
|
87
|
+
# TODO: Implement service logic here
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.call(**args)
|
|
91
|
+
new(**args).call
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
end
|
|
96
|
+
RUBY
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Spec template
|
|
100
|
+
# @return [String] Spec template content
|
|
101
|
+
def spec_template
|
|
102
|
+
<<~RUBY
|
|
103
|
+
# frozen_string_literal: true
|
|
104
|
+
|
|
105
|
+
RSpec.describe #{@name} do
|
|
106
|
+
describe '#call' do
|
|
107
|
+
it 'returns expected result' do
|
|
108
|
+
result = described_class.call
|
|
109
|
+
expect(result).to be_nil
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
RUBY
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Class method for CLI
|
|
117
|
+
def self.generate(service_name, with_spec: true, template_version: "v1")
|
|
118
|
+
new(service_name, with_spec: with_spec, template_version: template_version).generate
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Stimulus controller generator for RailsForge
|
|
2
|
+
# Generates Stimulus JavaScript controllers
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Generators
|
|
8
|
+
# Stimulus controller generator
|
|
9
|
+
class StimulusControllerGenerator < BaseGenerator
|
|
10
|
+
# Template version
|
|
11
|
+
TEMPLATE_VERSION = "v1"
|
|
12
|
+
|
|
13
|
+
# Initialize the generator
|
|
14
|
+
# @param name [String] Controller name
|
|
15
|
+
# @param options [Hash] Generator options
|
|
16
|
+
def initialize(name, options = {})
|
|
17
|
+
super(name, options)
|
|
18
|
+
validate_name!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Generate Stimulus controller files
|
|
22
|
+
# @return [String] Success message
|
|
23
|
+
def generate
|
|
24
|
+
return "Not in a Rails application directory" unless @base_path
|
|
25
|
+
|
|
26
|
+
results = []
|
|
27
|
+
results << generate_controller
|
|
28
|
+
results << generate_spec if with_spec?
|
|
29
|
+
|
|
30
|
+
results.join("\n")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Validate controller name
|
|
36
|
+
def validate_name!
|
|
37
|
+
validate_name!(@name, /\A[A-Z][a-zA-Z0-9]*\z/, InvalidNameError)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Check if should generate spec
|
|
41
|
+
def with_spec?
|
|
42
|
+
@options[:spec] != false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Generate the JavaScript controller file
|
|
46
|
+
def generate_controller
|
|
47
|
+
controller_dir = File.join(@base_path, "app", "javascript", "controllers")
|
|
48
|
+
FileUtils.mkdir_p(controller_dir)
|
|
49
|
+
|
|
50
|
+
file_name = "#{underscore}_controller.js"
|
|
51
|
+
file_path = File.join(controller_dir, file_name)
|
|
52
|
+
|
|
53
|
+
return "Skipping controller (already exists)" if File.exist?(file_path)
|
|
54
|
+
|
|
55
|
+
# Try to load template
|
|
56
|
+
template_content = load_template
|
|
57
|
+
content = apply_template(template_content)
|
|
58
|
+
|
|
59
|
+
File.write(file_path, content)
|
|
60
|
+
"Created app/javascript/controllers/#{file_name}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Generate the controller spec
|
|
64
|
+
def generate_spec
|
|
65
|
+
spec_dir = File.join(@base_path, "spec", "javascripts", "controllers")
|
|
66
|
+
FileUtils.mkdir_p(spec_dir)
|
|
67
|
+
|
|
68
|
+
file_name = "#{underscore}_controller_spec.js"
|
|
69
|
+
file_path = File.join(spec_dir, file_name)
|
|
70
|
+
|
|
71
|
+
return "Skipping spec (already exists)" if File.exist?(file_path)
|
|
72
|
+
|
|
73
|
+
content = <<~JS
|
|
74
|
+
import { Application } from "@hotwired/stimulus"
|
|
75
|
+
import #{camelize}Controller from "../../../app/javascript/controllers/#{underscore}_controller"
|
|
76
|
+
|
|
77
|
+
describe("#{camelize}Controller", () => {
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
this.application = new Application()
|
|
80
|
+
this.application.register("#{underscore}", #{camelize}Controller)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
JS
|
|
84
|
+
|
|
85
|
+
File.write(file_path, content)
|
|
86
|
+
"Created spec/javascripts/controllers/#{file_name}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Load template content
|
|
90
|
+
def load_template
|
|
91
|
+
template_path = File.join(
|
|
92
|
+
File.dirname(__FILE__),
|
|
93
|
+
"..",
|
|
94
|
+
"templates",
|
|
95
|
+
template_version,
|
|
96
|
+
"stimulus_controller",
|
|
97
|
+
"template.rb"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if File.exist?(template_path)
|
|
101
|
+
File.read(template_path)
|
|
102
|
+
else
|
|
103
|
+
default_template
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Default template content
|
|
108
|
+
def default_template
|
|
109
|
+
<<~JS
|
|
110
|
+
// Stimulus controller: <%= class_name %>
|
|
111
|
+
import { Controller } from "@hotwired/stimulus"
|
|
112
|
+
|
|
113
|
+
export default class <%= class_name %> extends Controller {
|
|
114
|
+
connect() {
|
|
115
|
+
console.log("<%= class_name %> connected")
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
JS
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Apply template variables
|
|
122
|
+
def apply_template(content)
|
|
123
|
+
content
|
|
124
|
+
.gsub("<%= class_name %>", camelize)
|
|
125
|
+
.gsub("<%= underscore_name %>", underscore)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|