railsforge 1.0.1 → 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 +4 -4
- data/lib/railsforge/cli.rb +19 -5
- data/lib/railsforge/generators/api_generator.rb +392 -0
- data/lib/railsforge/generators/base_generator.rb +6 -1
- data/lib/railsforge/generators/form_generator.rb +180 -0
- data/lib/railsforge/generators/job_generator.rb +176 -0
- data/lib/railsforge/generators/policy_generator.rb +220 -0
- data/lib/railsforge/generators/presenter_generator.rb +173 -0
- data/lib/railsforge/generators/query_generator.rb +174 -0
- data/lib/railsforge/generators/serializer_generator.rb +166 -0
- data/lib/railsforge/loader.rb +14 -3
- data/lib/railsforge/version.rb +1 -1
- metadata +11 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ca67789d645855334565249db474d63bfde8607e031642c52a287924c55b9cf
|
|
4
|
+
data.tar.gz: 9517139f8e3224a17ebd875c6606a2ea244e183e289acc1599181eed13522f07
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db1e954e6df040c1856be4cee40357e781201d29a550f31c1dc8924d4c3e93678daecefa42c8e651d02ddef5dc04d4ebd702cb37cac2a0731111ddf0a6e03d13
|
|
7
|
+
data.tar.gz: d7f692d25425099a0ea48499655272a70cf77f517cbfdadbe54826c517aa9e989833774feca6fb7742d36e5aa8bdc47c5c9d9760b8a0130009716ea74298e6c6
|
data/lib/railsforge/cli.rb
CHANGED
|
@@ -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}
|
|
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}
|
|
253
|
+
"Generating component: #{name}\n" + RailsForge::Generators::ViewComponentGenerator.new(name, template_version: template_version).generate
|
|
246
254
|
when :stimulus
|
|
247
|
-
"Generating stimulus: #{name}
|
|
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,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.
|
|
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
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Form generator for RailsForge
|
|
2
|
+
# Generates form object files
|
|
3
|
+
|
|
4
|
+
require_relative 'base_generator'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Generators
|
|
8
|
+
# FormGenerator creates form files
|
|
9
|
+
class FormGenerator < BaseGenerator
|
|
10
|
+
# Error class for invalid form names
|
|
11
|
+
class InvalidFormNameError < StandardError; end
|
|
12
|
+
|
|
13
|
+
# Template version
|
|
14
|
+
TEMPLATE_VERSION = "v1"
|
|
15
|
+
|
|
16
|
+
# Initialize the generator
|
|
17
|
+
# @param name [String] Form name
|
|
18
|
+
# @param options [Hash] Generator options
|
|
19
|
+
def initialize(name, options = {})
|
|
20
|
+
super(name, options)
|
|
21
|
+
@template_version = options[:template_version] || TEMPLATE_VERSION
|
|
22
|
+
@with_spec = options.fetch(:with_spec, true)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Generate form files
|
|
26
|
+
# @return [String] Success message
|
|
27
|
+
def generate
|
|
28
|
+
return "Not in a Rails application directory" unless @base_path
|
|
29
|
+
|
|
30
|
+
validate_name!(@name)
|
|
31
|
+
|
|
32
|
+
results = []
|
|
33
|
+
results << generate_form
|
|
34
|
+
|
|
35
|
+
if @with_spec
|
|
36
|
+
results << generate_spec
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
"Form '#{@name}' generated successfully!\n" + results.join("\n")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Class method for CLI
|
|
43
|
+
def self.generate(form_name, with_spec: true, template_version: "v1")
|
|
44
|
+
new(form_name, with_spec: with_spec, template_version: template_version).generate
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Validate form name
|
|
50
|
+
def validate_name!(name)
|
|
51
|
+
raise InvalidFormNameError, "Form name cannot be empty" if name.nil? || name.strip.empty?
|
|
52
|
+
raise InvalidFormNameError, "Name must match pattern: /\\A[A-Z][a-zA-Z0-9]*\\z/" unless name =~ /\A[A-Z][a-zA-Z0-9]*\z/
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Generate form file
|
|
56
|
+
def generate_form
|
|
57
|
+
form_dir = File.join(@base_path, "app", "forms")
|
|
58
|
+
FileUtils.mkdir_p(form_dir)
|
|
59
|
+
|
|
60
|
+
file_name = "#{underscore}_form.rb"
|
|
61
|
+
file_path = File.join(form_dir, file_name)
|
|
62
|
+
|
|
63
|
+
return " Skipping form (already exists)" if File.exist?(file_path)
|
|
64
|
+
|
|
65
|
+
content = load_template
|
|
66
|
+
content = apply_template(content)
|
|
67
|
+
|
|
68
|
+
File.write(file_path, content)
|
|
69
|
+
" Created app/forms/#{file_name}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Generate spec file
|
|
73
|
+
def generate_spec
|
|
74
|
+
spec_dir = File.join(@base_path, "spec", "forms")
|
|
75
|
+
FileUtils.mkdir_p(spec_dir)
|
|
76
|
+
|
|
77
|
+
file_name = "#{underscore}_form_spec.rb"
|
|
78
|
+
file_path = File.join(spec_dir, file_name)
|
|
79
|
+
|
|
80
|
+
return " Skipping spec (already exists)" if File.exist?(file_path)
|
|
81
|
+
|
|
82
|
+
content = load_spec_template
|
|
83
|
+
content = apply_template(content)
|
|
84
|
+
|
|
85
|
+
File.write(file_path, content)
|
|
86
|
+
" Created spec/forms/#{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
|
+
"form",
|
|
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
|
+
# Load spec template
|
|
108
|
+
def load_spec_template
|
|
109
|
+
spec_path = File.join(
|
|
110
|
+
File.dirname(__FILE__),
|
|
111
|
+
"..",
|
|
112
|
+
"templates",
|
|
113
|
+
@template_version,
|
|
114
|
+
"form",
|
|
115
|
+
"spec_template.rb"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if File.exist?(spec_path)
|
|
119
|
+
File.read(spec_path)
|
|
120
|
+
else
|
|
121
|
+
default_spec_template
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Default template
|
|
126
|
+
def default_template
|
|
127
|
+
<<~RUBY
|
|
128
|
+
# Form class for #{underscore}
|
|
129
|
+
# Encapsulates form validation and processing
|
|
130
|
+
class #{camelize}Form
|
|
131
|
+
include ActiveModel::Model
|
|
132
|
+
|
|
133
|
+
attr_accessor :name, :email
|
|
134
|
+
|
|
135
|
+
validates :name, presence: true
|
|
136
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
137
|
+
|
|
138
|
+
def save
|
|
139
|
+
return false unless valid?
|
|
140
|
+
|
|
141
|
+
# TODO: Implement save logic
|
|
142
|
+
true
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
RUBY
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Default spec template
|
|
149
|
+
def default_spec_template
|
|
150
|
+
<<~RUBY
|
|
151
|
+
require 'rails_helper'
|
|
152
|
+
|
|
153
|
+
RSpec.describe #{camelize}Form do
|
|
154
|
+
describe 'validations' do
|
|
155
|
+
it 'validates presence of name' do
|
|
156
|
+
form = described_class.new(name: nil, email: 'test@example.com')
|
|
157
|
+
expect(form).not_to be_valid
|
|
158
|
+
expect(form.errors[:name]).to be_present
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'validates email format' do
|
|
162
|
+
form = described_class.new(name: 'Test', email: 'invalid')
|
|
163
|
+
expect(form).not_to be_valid
|
|
164
|
+
expect(form.errors[:email]).to be_present
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
RUBY
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Apply template variables
|
|
172
|
+
def apply_template(content)
|
|
173
|
+
content
|
|
174
|
+
.gsub("<%= name %>", @name)
|
|
175
|
+
.gsub("<%= name.camelize %>", camelize)
|
|
176
|
+
.gsub("<%= name.underscore %>", underscore)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|