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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 01eaa16a2df2e84f2ce395692e0b09bd8691d96762548c6d8524ebf0f7870466
4
- data.tar.gz: 8c80982d2fe51c9cc13acc5b580f43a9cabe162f2a09ccf5bcd9d5fe8ffe518b
3
+ metadata.gz: 1ca67789d645855334565249db474d63bfde8607e031642c52a287924c55b9cf
4
+ data.tar.gz: 9517139f8e3224a17ebd875c6606a2ea244e183e289acc1599181eed13522f07
5
5
  SHA512:
6
- metadata.gz: 3dd1707c9e98f715d55206aaa06872deb0764f6bf678618447a42a60485c3e089fe9ea7cd424cc35a6f2b1d8fbb83c23368147686daf09d9057b9e4187c29d7b
7
- data.tar.gz: beef673a82ca8a20306e6193b10a176d8433d42350a533c30271d22b9a8f111da2cb4793f0f3f2927d51dc799bacf4daba62da48e9dc62a558ff27bddc8897a6
6
+ metadata.gz: db1e954e6df040c1856be4cee40357e781201d29a550f31c1dc8924d4c3e93678daecefa42c8e651d02ddef5dc04d4ebd702cb37cac2a0731111ddf0a6e03d13
7
+ data.tar.gz: d7f692d25425099a0ea48499655272a70cf77f517cbfdadbe54826c517aa9e989833774feca6fb7742d36e5aa8bdc47c5c9d9760b8a0130009716ea74298e6c6
@@ -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,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
@@ -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