create-ruby-app 1.1.0 → 1.3.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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/CLAUDE.md +74 -0
  4. data/CODE_REVIEW.md +1659 -0
  5. data/LICENSE +13 -21
  6. data/README.md +5 -5
  7. data/REFACTORING_PLAN.md +543 -0
  8. data/bin/create-ruby-app +1 -3
  9. data/lib/create_ruby_app/actions/create_directories.rb +10 -2
  10. data/lib/create_ruby_app/actions/generate_files.rb +7 -4
  11. data/lib/create_ruby_app/actions/install_gems.rb +10 -2
  12. data/lib/create_ruby_app/actions/make_script_executable.rb +10 -2
  13. data/lib/create_ruby_app/actions/set_ruby_implementation.rb +52 -0
  14. data/lib/create_ruby_app/app.rb +9 -8
  15. data/lib/create_ruby_app/cli.rb +58 -41
  16. data/lib/create_ruby_app/templates/Gemfile.erb +1 -3
  17. data/lib/create_ruby_app/templates/lib_file.erb +0 -2
  18. data/lib/create_ruby_app/templates/script_file.erb +0 -2
  19. data/lib/create_ruby_app/templates/spec_helper.erb +0 -2
  20. data/lib/create_ruby_app/version.rb +1 -3
  21. data/lib/create_ruby_app.rb +1 -3
  22. data/spec/integration/app_creation_spec.rb +170 -0
  23. data/spec/lib/create_ruby_app/actions/create_directories_spec.rb +1 -3
  24. data/spec/lib/create_ruby_app/actions/generate_files_spec.rb +13 -20
  25. data/spec/lib/create_ruby_app/actions/install_gems_spec.rb +1 -3
  26. data/spec/lib/create_ruby_app/actions/make_script_executable_spec.rb +1 -3
  27. data/spec/lib/create_ruby_app/actions/set_ruby_implementation_spec.rb +194 -0
  28. data/spec/lib/create_ruby_app/app_spec.rb +4 -4
  29. data/spec/lib/create_ruby_app/cli_spec.rb +112 -0
  30. data/spec/spec_helper.rb +6 -2
  31. metadata +52 -20
  32. data/lib/create_ruby_app/actions/null_action.rb +0 -9
data/CODE_REVIEW.md ADDED
@@ -0,0 +1,1659 @@
1
+ # Code Review & Improvement Plan for create-ruby-app
2
+
3
+ ## Overview
4
+
5
+ The codebase is well-structured with a clean action-based architecture.
6
+ Test coverage is at 64.71% (target: 90%). The code follows Ruby idioms
7
+ reasonably well but has opportunities for improvement in immutability,
8
+ side effects, DRY principles, and SOLID adherence.
9
+
10
+ ---
11
+
12
+ ## Critical Issues
13
+
14
+ ### 1. Mutation of App State
15
+
16
+ **Location:** `lib/create_ruby_app/actions/set_ruby_implementation.rb:23`
17
+
18
+ **Problem:** `SetRubyImplementation` mutates the `app` object by calling
19
+ `app.version=`, violating immutability principles and creating hidden
20
+ side effects.
21
+
22
+ **Impact:** Makes reasoning about state difficult, complicates testing,
23
+ breaks functional programming principles.
24
+
25
+ **Before:**
26
+ ```ruby
27
+ def call
28
+ return app if app.version
29
+
30
+ RUBY_IMPLEMENTATIONS.find do |ruby_implementation|
31
+ stdout, status = fetch_ruby_implementation(ruby_implementation)
32
+
33
+ if status.success? && !stdout.empty?
34
+ app.version = transform_output_to_ruby_version(stdout)
35
+ return app
36
+ end
37
+ end
38
+
39
+ raise NoRubyImplementationFoundError
40
+ end
41
+ ```
42
+
43
+ **After:**
44
+ ```ruby
45
+ def call
46
+ return app.version if app.version
47
+
48
+ RUBY_IMPLEMENTATIONS.each do |ruby_implementation|
49
+ stdout, status = fetch_ruby_implementation(ruby_implementation)
50
+
51
+ if status.success? && !stdout.empty?
52
+ return transform_output_to_ruby_version(stdout)
53
+ end
54
+ end
55
+
56
+ raise NoRubyImplementationFoundError
57
+ end
58
+
59
+ # In App#run!:
60
+ def run!
61
+ @version = Actions::SetRubyImplementation.call(self) unless @version
62
+ # ... rest of actions
63
+ end
64
+ ```
65
+
66
+ **Benefit:** Pure function, testable, no side effects, clear data flow.
67
+
68
+ ---
69
+
70
+ ### 2. Test Coverage Gap (64.71% vs 90% target)
71
+
72
+ **Problem:** Missing test coverage for:
73
+ - `CLI.call` integration path
74
+ - `App#with_logger` private method behavior
75
+ - Error handling paths in actions
76
+ - Template rendering edge cases
77
+
78
+ **Impact:** Insufficient confidence in refactoring, potential bugs in
79
+ uncovered code paths.
80
+
81
+ **Example Missing Test:**
82
+ ```ruby
83
+ # spec/lib/create_ruby_app/actions/install_gems_spec.rb
84
+ RSpec.describe CreateRubyApp::Actions::InstallGems do
85
+ describe ".call" do
86
+ context "when bundle install fails" do
87
+ it "raises a GemInstallationError" do
88
+ app = instance_double(CreateRubyApp::App, name: "test_app")
89
+
90
+ allow(Dir).to receive(:chdir).and_yield
91
+ allow_any_instance_of(Object).to receive(:system)
92
+ .and_return(false)
93
+
94
+ expect { described_class.call(app) }
95
+ .to raise_error(
96
+ CreateRubyApp::Actions::GemInstallationError
97
+ )
98
+ end
99
+ end
100
+ end
101
+ end
102
+ ```
103
+
104
+ **Benefit:** Safer refactoring, fewer production bugs, meets project
105
+ standards.
106
+
107
+ ---
108
+
109
+ ## Design & Architecture Issues
110
+
111
+ ### 3. Inconsistent Action Interface Pattern
112
+
113
+ **Location:** All action files in
114
+ `lib/create_ruby_app/actions/*.rb:11-13`
115
+
116
+ **Problem:** Actions have both class method `.call(app)` and instance
117
+ method `#call`, creating unnecessary indirection. The class method
118
+ instantiates then immediately calls instance method.
119
+
120
+ **Impact:** Boilerplate in every action, unclear whether to use class
121
+ or instance, harder to test.
122
+
123
+ **Before:**
124
+ ```ruby
125
+ class CreateDirectories
126
+ def initialize(app)
127
+ @app = app
128
+ end
129
+
130
+ def self.call(app)
131
+ new(app).call
132
+ end
133
+
134
+ def call
135
+ [
136
+ Pathname.new("#{app.name}/bin"),
137
+ Pathname.new("#{app.name}/lib/#{app.name}"),
138
+ Pathname.new("#{app.name}/spec/lib/#{app.name}")
139
+ ].each(&FileUtils.method(:mkdir_p))
140
+ end
141
+
142
+ attr_reader :app
143
+ end
144
+ ```
145
+
146
+ **After:**
147
+ ```ruby
148
+ class CreateDirectories
149
+ def self.call(app)
150
+ [
151
+ Pathname.new("#{app.name}/bin"),
152
+ Pathname.new("#{app.name}/lib/#{app.name}"),
153
+ Pathname.new("#{app.name}/spec/lib/#{app.name}")
154
+ ].each(&FileUtils.method(:mkdir_p))
155
+ end
156
+ end
157
+ ```
158
+
159
+ **Benefit:** Less boilerplate, clearer API, easier to understand and
160
+ test.
161
+
162
+ ---
163
+
164
+ ### 4. God Method: App#run! Violates Single Responsibility Principle
165
+
166
+ **Location:** `lib/create_ruby_app/app.rb:17-27`
167
+
168
+ **Problem:** `run!` method is hardcoded with specific actions and
169
+ logging messages. Adding/removing/reordering actions requires modifying
170
+ this method. Mixing orchestration with logging concerns.
171
+
172
+ **Impact:** Violates Open/Closed Principle, difficult to extend, tight
173
+ coupling, hard to test individual flow variations.
174
+
175
+ **Before:**
176
+ ```ruby
177
+ def run!
178
+ with_logger("Creating directories...", Actions::CreateDirectories)
179
+ with_logger(
180
+ "Setting Ruby implementations...",
181
+ Actions::SetRubyImplementation
182
+ )
183
+ with_logger("Generating files...", Actions::GenerateFiles)
184
+ with_logger("Making script executable...", Actions::MakeScriptExecutable)
185
+ with_logger("Installing gems...", Actions::InstallGems)
186
+ with_logger("Happy hacking!", ->(_) {})
187
+ end
188
+ ```
189
+
190
+ **After:**
191
+ ```ruby
192
+ def run!
193
+ pipeline.each do |step|
194
+ logger.info(step[:message])
195
+ step[:action].call(self)
196
+ end
197
+ end
198
+
199
+ def pipeline
200
+ @pipeline ||= [
201
+ {
202
+ message: "Creating directories...",
203
+ action: Actions::CreateDirectories
204
+ },
205
+ {
206
+ message: "Setting Ruby implementations...",
207
+ action: Actions::SetRubyImplementation
208
+ },
209
+ {
210
+ message: "Generating files...",
211
+ action: Actions::GenerateFiles
212
+ },
213
+ {
214
+ message: "Making script executable...",
215
+ action: Actions::MakeScriptExecutable
216
+ },
217
+ {
218
+ message: "Installing gems...",
219
+ action: Actions::InstallGems
220
+ }
221
+ ]
222
+ end
223
+
224
+ def log_completion
225
+ logger.info("Happy hacking!")
226
+ end
227
+ ```
228
+
229
+ **Benefit:** Extensible, testable, follows SOLID, allows dependency
230
+ injection of action pipeline.
231
+
232
+ ---
233
+
234
+ ### 5. Logging Responsibility Misplaced
235
+
236
+ **Location:** `lib/create_ruby_app/app.rb:38-41`
237
+
238
+ **Problem:** `App#with_logger` couples logging to action execution.
239
+ Actions don't know they're being logged, but App controls this as a
240
+ cross-cutting concern in a non-standard way.
241
+
242
+ **Impact:** Violates Single Responsibility, makes it hard to change
243
+ logging strategy, reduces flexibility.
244
+
245
+ **Before:**
246
+ ```ruby
247
+ def with_logger(text, action)
248
+ logger.info(text)
249
+ action.call(self)
250
+ end
251
+ ```
252
+
253
+ **After (Option 1 - Decorator Pattern):**
254
+ ```ruby
255
+ class LoggedAction
256
+ def initialize(action, message, logger)
257
+ @action = action
258
+ @message = message
259
+ @logger = logger
260
+ end
261
+
262
+ def call(app)
263
+ @logger.info(@message)
264
+ @action.call(app)
265
+ end
266
+ end
267
+
268
+ # Usage in App#run!:
269
+ def run!
270
+ actions.each { |action| action.call(self) }
271
+ logger.info("Happy hacking!")
272
+ end
273
+
274
+ def actions
275
+ [
276
+ LoggedAction.new(
277
+ Actions::CreateDirectories,
278
+ "Creating directories...",
279
+ logger
280
+ ),
281
+ # ... etc
282
+ ]
283
+ end
284
+ ```
285
+
286
+ **After (Option 2 - Pipeline Executor):**
287
+ ```ruby
288
+ class ActionPipeline
289
+ def initialize(steps, logger)
290
+ @steps = steps
291
+ @logger = logger
292
+ end
293
+
294
+ def execute(app)
295
+ @steps.each do |step|
296
+ @logger.info(step[:message])
297
+ step[:action].call(app)
298
+ end
299
+ end
300
+ end
301
+
302
+ # Usage:
303
+ def run!
304
+ ActionPipeline.new(pipeline, logger).execute(self)
305
+ end
306
+ ```
307
+
308
+ **Benefit:** Separation of concerns, flexible logging strategies,
309
+ cleaner App class.
310
+
311
+ ---
312
+
313
+ ### 6. Tight Coupling: Actions Know Too Much About App Structure
314
+
315
+ **Location:** All action classes
316
+
317
+ **Problem:** Actions directly access `app.name`, `app.gems`,
318
+ `app.version`. Changes to App internal structure ripple through all
319
+ actions.
320
+
321
+ **Impact:** High coupling, difficult to refactor App, violates Law of
322
+ Demeter.
323
+
324
+ **Before:**
325
+ ```ruby
326
+ class CreateDirectories
327
+ def self.call(app)
328
+ [
329
+ Pathname.new("#{app.name}/bin"),
330
+ Pathname.new("#{app.name}/lib/#{app.name}"),
331
+ Pathname.new("#{app.name}/spec/lib/#{app.name}")
332
+ ].each(&FileUtils.method(:mkdir_p))
333
+ end
334
+ end
335
+ ```
336
+
337
+ **After (Option 1 - Pass Only Required Data):**
338
+ ```ruby
339
+ class CreateDirectories
340
+ def self.call(project_name:)
341
+ [
342
+ Pathname.new("#{project_name}/bin"),
343
+ Pathname.new("#{project_name}/lib/#{project_name}"),
344
+ Pathname.new("#{project_name}/spec/lib/#{project_name}")
345
+ ].each(&FileUtils.method(:mkdir_p))
346
+ end
347
+ end
348
+
349
+ # In App#run!:
350
+ Actions::CreateDirectories.call(project_name: name)
351
+ ```
352
+
353
+ **After (Option 2 - Context Object):**
354
+ ```ruby
355
+ class AppContext
356
+ attr_reader :project_name, :ruby_version, :gems, :paths
357
+
358
+ def initialize(name:, version:, gems:)
359
+ @project_name = name
360
+ @ruby_version = version
361
+ @gems = gems
362
+ @paths = PathBuilder.new(name)
363
+ end
364
+ end
365
+
366
+ class CreateDirectories
367
+ def self.call(context)
368
+ context.paths.directories.each(&FileUtils.method(:mkdir_p))
369
+ end
370
+ end
371
+ ```
372
+
373
+ **Benefit:** Loose coupling, easier to refactor, clearer dependencies,
374
+ better testability.
375
+
376
+ ---
377
+
378
+ ## Code Quality Issues
379
+
380
+ ### 7. Magic Empty Lambda as Final Action
381
+
382
+ **Location:** `lib/create_ruby_app/app.rb:26`
383
+
384
+ **Problem:** `with_logger("Happy hacking!", ->(_) {})` uses empty
385
+ lambda to show completion message. Unclear intent, breaks action
386
+ pattern consistency.
387
+
388
+ **Impact:** Confusing, fragile, violates Principle of Least Surprise.
389
+
390
+ **Before:**
391
+ ```ruby
392
+ def run!
393
+ with_logger("Creating directories...", Actions::CreateDirectories)
394
+ # ... other actions
395
+ with_logger("Happy hacking!", ->(_) {})
396
+ end
397
+ ```
398
+
399
+ **After (Option 1):**
400
+ ```ruby
401
+ def run!
402
+ with_logger("Creating directories...", Actions::CreateDirectories)
403
+ # ... other actions
404
+ log_completion
405
+ end
406
+
407
+ private
408
+
409
+ def log_completion
410
+ logger.info("Happy hacking!")
411
+ end
412
+ ```
413
+
414
+ **After (Option 2):**
415
+ ```ruby
416
+ class CompletionAction
417
+ def self.call(_app)
418
+ # No-op action for consistency
419
+ end
420
+ end
421
+
422
+ def run!
423
+ with_logger("Creating directories...", Actions::CreateDirectories)
424
+ # ... other actions
425
+ with_logger("Happy hacking!", CompletionAction)
426
+ end
427
+ ```
428
+
429
+ **Benefit:** Clear intent, consistent pattern, self-documenting code.
430
+
431
+ ---
432
+
433
+ ### 8. Hardcoded Constants Should Be Configurable
434
+
435
+ **Location:**
436
+ `lib/create_ruby_app/actions/set_ruby_implementation.rb:41-47`
437
+
438
+ **Problem:** `RUBY_IMPLEMENTATIONS` array is hardcoded. No way to
439
+ extend or modify Ruby implementation search order without modifying
440
+ source.
441
+
442
+ **Impact:** Not extensible for new Ruby implementations or custom
443
+ search orders.
444
+
445
+ **Before:**
446
+ ```ruby
447
+ RUBY_IMPLEMENTATIONS = %w[
448
+ ruby
449
+ truffleruby
450
+ jruby
451
+ mruby
452
+ rubinius
453
+ ].freeze
454
+ ```
455
+
456
+ **After:**
457
+ ```ruby
458
+ class SetRubyImplementation
459
+ DEFAULT_IMPLEMENTATIONS = %w[
460
+ ruby
461
+ truffleruby
462
+ jruby
463
+ mruby
464
+ rubinius
465
+ ].freeze
466
+
467
+ def self.call(app, implementations: DEFAULT_IMPLEMENTATIONS)
468
+ return app.version if app.version
469
+
470
+ implementations.each do |ruby_implementation|
471
+ stdout, status = fetch_ruby_implementation(ruby_implementation)
472
+
473
+ if status.success? && !stdout.empty?
474
+ return transform_output_to_ruby_version(stdout)
475
+ end
476
+ end
477
+
478
+ raise NoRubyImplementationFoundError
479
+ end
480
+ end
481
+
482
+ # In App initialization:
483
+ def initialize(
484
+ name: "app",
485
+ gems: [],
486
+ version: nil,
487
+ logger: Logger.new($stdout),
488
+ ruby_implementations: nil
489
+ )
490
+ @name = name
491
+ @gems = gems
492
+ @version = version
493
+ @logger = logger
494
+ @ruby_implementations = ruby_implementations
495
+ end
496
+ ```
497
+
498
+ **Benefit:** Flexible, extensible, follows Open/Closed Principle.
499
+
500
+ ---
501
+
502
+ ### 9. Non-functional String Manipulation in GenerateFiles#gemfile
503
+
504
+ **Location:** `lib/create_ruby_app/actions/generate_files.rb:38-44`
505
+
506
+ **Problem:** Inline block with sorting, mapping, and joining makes code
507
+ harder to read. The logic for formatting gems is mixed with template
508
+ data preparation.
509
+
510
+ **Impact:** Reduced readability, difficult to test gem formatting
511
+ independently, violates Single Responsibility.
512
+
513
+ **Before:**
514
+ ```ruby
515
+ def gemfile
516
+ generate_file(file: "Gemfile.erb", locals: {
517
+ gems: app
518
+ .gems
519
+ .sort
520
+ .map {|gem| "gem \"#{gem}\"" }.join("\n")
521
+ })
522
+ end
523
+ ```
524
+
525
+ **After:**
526
+ ```ruby
527
+ def gemfile
528
+ generate_file(
529
+ file: "Gemfile.erb",
530
+ locals: { gems: format_gems_for_gemfile(app.gems) }
531
+ )
532
+ end
533
+
534
+ private
535
+
536
+ def format_gems_for_gemfile(gems)
537
+ gems
538
+ .sort
539
+ .map { |gem| "gem \"#{gem}\"" }
540
+ .join("\n")
541
+ end
542
+ ```
543
+
544
+ **Benefit:** Readable, testable, follows Single Responsibility
545
+ Principle.
546
+
547
+ ---
548
+
549
+ ### 10. Path Construction Scattered Across Actions
550
+
551
+ **Location:** Multiple files:
552
+ - `lib/create_ruby_app/actions/create_directories.rb:17-19`
553
+ - `lib/create_ruby_app/actions/make_script_executable.rb:15`
554
+ - `lib/create_ruby_app/actions/generate_files.rb:56`
555
+ - `lib/create_ruby_app/actions/install_gems.rb:13`
556
+
557
+ **Problem:** String interpolation like `"#{app.name}/bin/#{app.name}"`
558
+ appears in multiple actions.
559
+
560
+ **Impact:** DRY violation, inconsistent path handling, error-prone,
561
+ hard to change directory structure.
562
+
563
+ **Before:**
564
+ ```ruby
565
+ # In CreateDirectories:
566
+ Pathname.new("#{app.name}/bin")
567
+ Pathname.new("#{app.name}/lib/#{app.name}")
568
+
569
+ # In MakeScriptExecutable:
570
+ FileUtils.chmod("+x", "#{app.name}/bin/#{app.name}")
571
+
572
+ # In GenerateFiles:
573
+ File.write("#{app.name}/#{path}", content)
574
+
575
+ # In InstallGems:
576
+ Dir.chdir(app.name) { system("bundle", "install") }
577
+ ```
578
+
579
+ **After:**
580
+ ```ruby
581
+ # In App class:
582
+ def project_root
583
+ @project_root ||= Pathname.new(name)
584
+ end
585
+
586
+ def bin_dir
587
+ project_root.join("bin")
588
+ end
589
+
590
+ def bin_script_path
591
+ bin_dir.join(name)
592
+ end
593
+
594
+ def lib_dir
595
+ project_root.join("lib")
596
+ end
597
+
598
+ def lib_subdir
599
+ lib_dir.join(name)
600
+ end
601
+
602
+ def spec_dir
603
+ project_root.join("spec")
604
+ end
605
+
606
+ def spec_lib_dir
607
+ spec_dir.join("lib", name)
608
+ end
609
+
610
+ def file_path(relative_path)
611
+ project_root.join(relative_path)
612
+ end
613
+
614
+ # Usage in actions:
615
+ class CreateDirectories
616
+ def self.call(app)
617
+ [
618
+ app.bin_dir,
619
+ app.lib_subdir,
620
+ app.spec_lib_dir
621
+ ].each(&FileUtils.method(:mkdir_p))
622
+ end
623
+ end
624
+
625
+ class MakeScriptExecutable
626
+ def self.call(app)
627
+ FileUtils.chmod("+x", app.bin_script_path)
628
+ end
629
+ end
630
+
631
+ class GenerateFiles
632
+ def create_file(path:, content:)
633
+ File.write(app.file_path(path), content)
634
+ end
635
+ end
636
+
637
+ class InstallGems
638
+ def self.call(app)
639
+ Dir.chdir(app.project_root) { system("bundle", "install") }
640
+ end
641
+ end
642
+ ```
643
+
644
+ **Benefit:** DRY, consistent paths, single source of truth, easier to
645
+ test and modify.
646
+
647
+ ---
648
+
649
+ ### 11. Mixed Concerns in GenerateFiles
650
+
651
+ **Location:** `lib/create_ruby_app/actions/generate_files.rb`
652
+
653
+ **Problem:** Class handles both file generation logic AND template
654
+ rendering AND path construction. Methods like `script_file`, `lib_file`
655
+ are public but only used internally.
656
+
657
+ **Impact:** Violates Single Responsibility, unclear API, difficult to
658
+ test components independently.
659
+
660
+ **Before:**
661
+ ```ruby
662
+ class GenerateFiles
663
+ def call
664
+ generate_files
665
+ end
666
+
667
+ def script_file
668
+ generate_file(file: "script_file.erb", locals: {})
669
+ end
670
+
671
+ def lib_file
672
+ generate_file(file: "lib_file.erb", locals: { app: app.classify_name })
673
+ end
674
+
675
+ # ... more public methods that should be private
676
+
677
+ private
678
+
679
+ def generate_file(file:, locals: [])
680
+ ERB
681
+ .new(read_file(file), trim_mode: TRIM_MODE)
682
+ .result_with_hash(locals: locals)
683
+ end
684
+
685
+ def read_file(file)
686
+ Pathname.new(__FILE__)
687
+ .dirname
688
+ .join("#{TEMPLATES_DIR}/#{file}")
689
+ .read
690
+ end
691
+
692
+ def files
693
+ {
694
+ "bin/#{app.name}" => script_file,
695
+ "lib/#{app.name}.rb" => lib_file,
696
+ # ...
697
+ }
698
+ end
699
+ end
700
+ ```
701
+
702
+ **After:**
703
+ ```ruby
704
+ # Separate template renderer:
705
+ class TemplateRenderer
706
+ TRIM_MODE = "<>"
707
+ TEMPLATES_DIR = File.expand_path("../templates", __dir__)
708
+
709
+ def self.render(template_name, locals = {})
710
+ template_path = File.join(TEMPLATES_DIR, "#{template_name}.erb")
711
+ template_content = File.read(template_path)
712
+
713
+ ERB
714
+ .new(template_content, trim_mode: TRIM_MODE)
715
+ .result_with_hash(locals: locals)
716
+ end
717
+
718
+ private_constant :TRIM_MODE, :TEMPLATES_DIR
719
+ end
720
+
721
+ # Simplified GenerateFiles:
722
+ class GenerateFiles
723
+ def self.call(app)
724
+ files(app).each do |path, content|
725
+ File.write(app.file_path(path), content)
726
+ end
727
+ end
728
+
729
+ def self.files(app)
730
+ {
731
+ "bin/#{app.name}" => script_file,
732
+ "lib/#{app.name}.rb" => lib_file(app),
733
+ "spec/spec_helper.rb" => spec_helper_file(app),
734
+ ".ruby-version" => ruby_version_file(app),
735
+ "Gemfile" => gemfile(app)
736
+ }
737
+ end
738
+
739
+ private_class_method def self.script_file
740
+ TemplateRenderer.render("script_file")
741
+ end
742
+
743
+ private_class_method def self.lib_file(app)
744
+ TemplateRenderer.render("lib_file", app: app.classify_name)
745
+ end
746
+
747
+ private_class_method def self.spec_helper_file(app)
748
+ TemplateRenderer.render("spec_helper", app: app.name)
749
+ end
750
+
751
+ private_class_method def self.ruby_version_file(app)
752
+ TemplateRenderer.render("ruby-version", version: app.version)
753
+ end
754
+
755
+ private_class_method def self.gemfile(app)
756
+ TemplateRenderer.render(
757
+ "Gemfile",
758
+ gems: format_gems(app.gems)
759
+ )
760
+ end
761
+
762
+ private_class_method def self.format_gems(gems)
763
+ gems.sort.map { |gem| "gem \"#{gem}\"" }.join("\n")
764
+ end
765
+ end
766
+ ```
767
+
768
+ **Benefit:** Single Responsibility, clearer separation, easier to test,
769
+ better encapsulation.
770
+
771
+ ---
772
+
773
+ ### 12. Error Handling Inconsistency
774
+
775
+ **Location:** Various action files
776
+
777
+ **Problem:** Only `SetRubyImplementation` has custom error class. Other
778
+ actions fail silently or raise system errors (FileUtils, System calls).
779
+
780
+ **Impact:** Inconsistent error handling, difficult to rescue specific
781
+ failures, poor user experience.
782
+
783
+ **Before:**
784
+ ```ruby
785
+ # Only one custom error:
786
+ class NoRubyImplementationFoundError < StandardError; end
787
+
788
+ # Other actions have no error handling:
789
+ class InstallGems
790
+ def self.call(app)
791
+ Dir.chdir(app.name) { system("bundle", "install") }
792
+ end
793
+ end
794
+ ```
795
+
796
+ **After:**
797
+ ```ruby
798
+ # lib/create_ruby_app/errors.rb
799
+ module CreateRubyApp
800
+ class Error < StandardError; end
801
+
802
+ class NoRubyImplementationFoundError < Error; end
803
+ class DirectoryCreationError < Error; end
804
+ class FileGenerationError < Error; end
805
+ class GemInstallationError < Error; end
806
+ class ScriptExecutableError < Error; end
807
+ end
808
+
809
+ # Usage in actions:
810
+ class InstallGems
811
+ def self.call(app)
812
+ Dir.chdir(app.project_root) do
813
+ success = system("bundle", "install")
814
+
815
+ unless success
816
+ raise GemInstallationError,
817
+ "Failed to install gems for #{app.name}. " \
818
+ "Please check your Gemfile and try again."
819
+ end
820
+ end
821
+ rescue Errno::ENOENT => e
822
+ raise GemInstallationError,
823
+ "Bundle command not found: #{e.message}"
824
+ end
825
+ end
826
+
827
+ class CreateDirectories
828
+ def self.call(app)
829
+ [
830
+ app.bin_dir,
831
+ app.lib_subdir,
832
+ app.spec_lib_dir
833
+ ].each do |dir|
834
+ FileUtils.mkdir_p(dir)
835
+ end
836
+ rescue Errno::EACCES => e
837
+ raise DirectoryCreationError,
838
+ "Permission denied creating directories: #{e.message}"
839
+ rescue StandardError => e
840
+ raise DirectoryCreationError,
841
+ "Failed to create directories: #{e.message}"
842
+ end
843
+ end
844
+ ```
845
+
846
+ **Benefit:** Consistent error handling, better user experience, easier
847
+ to debug, more robust.
848
+
849
+ ---
850
+
851
+ ### 13. System Call Without Error Handling
852
+
853
+ **Location:** `lib/create_ruby_app/actions/install_gems.rb:13`
854
+
855
+ **Problem:** `system("bundle", "install")` doesn't check return value.
856
+ If bundler fails, execution continues silently.
857
+
858
+ **Impact:** Creates incomplete/broken projects, silent failures, poor
859
+ user experience.
860
+
861
+ **Before:**
862
+ ```ruby
863
+ class InstallGems
864
+ def self.call(app)
865
+ Dir.chdir(app.name) { system("bundle", "install") }
866
+ end
867
+ end
868
+ ```
869
+
870
+ **After:**
871
+ ```ruby
872
+ class InstallGems
873
+ def self.call(app)
874
+ Dir.chdir(app.project_root) do
875
+ success = system("bundle", "install")
876
+
877
+ unless success
878
+ raise GemInstallationError,
879
+ "Bundle install failed with exit code #{$?.exitstatus}. " \
880
+ "Please check your Gemfile and ensure bundler is installed."
881
+ end
882
+ end
883
+ rescue Errno::ENOENT
884
+ raise GemInstallationError,
885
+ "Bundler is not installed. Please run: gem install bundler"
886
+ end
887
+ end
888
+ ```
889
+
890
+ **Benefit:** Catches failures early, better user experience, more
891
+ robust.
892
+
893
+ ---
894
+
895
+ ### 14. Template Constants Are Implementation Details
896
+
897
+ **Location:** `lib/create_ruby_app/actions/generate_files.rb:82-83`
898
+
899
+ **Problem:** `TRIM_MODE` and `TEMPLATES_DIR` are public constants but
900
+ are implementation details.
901
+
902
+ **Impact:** Pollutes constant namespace, can be accidentally overridden,
903
+ unclear why they're public.
904
+
905
+ **Before:**
906
+ ```ruby
907
+ class GenerateFiles
908
+ # ... methods ...
909
+
910
+ TRIM_MODE = "<>"
911
+ TEMPLATES_DIR = "../templates"
912
+ end
913
+ ```
914
+
915
+ **After:**
916
+ ```ruby
917
+ class GenerateFiles
918
+ TRIM_MODE = "<>"
919
+ TEMPLATES_DIR = "../templates"
920
+
921
+ private_constant :TRIM_MODE, :TEMPLATES_DIR
922
+
923
+ # ... methods ...
924
+ end
925
+ ```
926
+
927
+ **Benefit:** Better encapsulation, clearer API, prevents accidental
928
+ modification.
929
+
930
+ ---
931
+
932
+ ### 15. Unused Empty Class
933
+
934
+ **Location:** `lib/create_ruby_app.rb:11-12`
935
+
936
+ **Problem:** `CreateRubyApp::CreateRubyApp` class is empty and unused.
937
+
938
+ **Impact:** Dead code, confusing namespace, unclear purpose.
939
+
940
+ **Before:**
941
+ ```ruby
942
+ module CreateRubyApp
943
+ class CreateRubyApp
944
+ end
945
+ end
946
+ ```
947
+
948
+ **After:**
949
+ ```ruby
950
+ # Remove the empty class entirely
951
+ ```
952
+
953
+ **Benefit:** Cleaner codebase, less confusion, follows YAGNI principle.
954
+
955
+ ---
956
+
957
+ ## Minor Improvements
958
+
959
+ ### 16. classify_name Could Use ActiveSupport-style Method
960
+
961
+ **Location:** `lib/create_ruby_app/app.rb:29-31`
962
+
963
+ **Problem:** Manual string manipulation. Ruby/Rails has established
964
+ patterns for this.
965
+
966
+ **Impact:** Minor: reinventing the wheel, less idiomatic.
967
+
968
+ **Before:**
969
+ ```ruby
970
+ def classify_name
971
+ name.split("_").collect(&:capitalize).join
972
+ end
973
+ ```
974
+
975
+ **After:**
976
+ ```ruby
977
+ def classify_name
978
+ name.split("_").map(&:capitalize).join
979
+ end
980
+
981
+ # Or extract to a separate concern:
982
+ module StringClassifier
983
+ def self.classify(string)
984
+ string.split("_").map(&:capitalize).join
985
+ end
986
+ end
987
+
988
+ def classify_name
989
+ StringClassifier.classify(name)
990
+ end
991
+ ```
992
+
993
+ **Benefit:** More idiomatic, potentially more robust edge case handling.
994
+
995
+ ---
996
+
997
+ ### 17. Gemspec Has Outdated Date
998
+
999
+ **Location:** `create_ruby_app.gemspec:8`
1000
+
1001
+ **Problem:** `s.date = "2019-03-11"` is hardcoded and outdated.
1002
+
1003
+ **Impact:** Misleading metadata, appears unmaintained.
1004
+
1005
+ **Before:**
1006
+ ```ruby
1007
+ Gem::Specification.new do |s|
1008
+ s.name = "create-ruby-app"
1009
+ s.version = CreateRubyApp::VERSION
1010
+ s.date = "2019-03-11"
1011
+ # ...
1012
+ end
1013
+ ```
1014
+
1015
+ **After:**
1016
+ ```ruby
1017
+ Gem::Specification.new do |s|
1018
+ s.name = "create-ruby-app"
1019
+ s.version = CreateRubyApp::VERSION
1020
+ # Let RubyGems set the date automatically
1021
+ # ...
1022
+ end
1023
+ ```
1024
+
1025
+ **Benefit:** Accurate metadata, appears maintained.
1026
+
1027
+ ---
1028
+
1029
+ ### 18. Dry::CLI::Registry Could Use Module Inclusion
1030
+
1031
+ **Location:** `lib/create_ruby_app/cli.rb:4-5`
1032
+
1033
+ **Problem:** `Commands` module extends `Dry::CLI::Registry`, but it's
1034
+ not immediately clear this is necessary vs include.
1035
+
1036
+ **Impact:** Minor: potential confusion for maintainers unfamiliar with
1037
+ dry-cli.
1038
+
1039
+ **Before:**
1040
+ ```ruby
1041
+ module Commands
1042
+ extend Dry::CLI::Registry
1043
+
1044
+ class Version < Dry::CLI::Command
1045
+ # ...
1046
+ end
1047
+ end
1048
+ ```
1049
+
1050
+ **After:**
1051
+ ```ruby
1052
+ module Commands
1053
+ # Extend (not include) is required for Dry::CLI registry pattern
1054
+ extend Dry::CLI::Registry
1055
+
1056
+ class Version < Dry::CLI::Command
1057
+ # ...
1058
+ end
1059
+ end
1060
+ ```
1061
+
1062
+ **Benefit:** Clearer intent, easier maintenance.
1063
+
1064
+ ---
1065
+
1066
+ ### 19. parse_gems Method Could Be More Functional
1067
+
1068
+ **Location:** `lib/create_ruby_app/cli.rb:53-57`
1069
+
1070
+ **Problem:** Multiple transformations chained, but early return with
1071
+ ternary operator breaks functional flow.
1072
+
1073
+ **Impact:** Minor: slightly less readable, mixed paradigms.
1074
+
1075
+ **Before:**
1076
+ ```ruby
1077
+ def parse_gems(gems_string)
1078
+ return [] if gems_string.nil? || gems_string.strip.empty?
1079
+
1080
+ gems_string.split(",").map(&:strip).reject(&:empty?)
1081
+ end
1082
+ ```
1083
+
1084
+ **After:**
1085
+ ```ruby
1086
+ def parse_gems(gems_string)
1087
+ gems_string
1088
+ .to_s
1089
+ .split(",")
1090
+ .map(&:strip)
1091
+ .reject(&:empty?)
1092
+ end
1093
+
1094
+ # Or more Ruby-idiomatic with safe navigation:
1095
+ def parse_gems(gems_string)
1096
+ return [] unless gems_string
1097
+
1098
+ gems_string
1099
+ .split(",")
1100
+ .map(&:strip)
1101
+ .reject(&:empty?)
1102
+ end
1103
+ ```
1104
+
1105
+ **Benefit:** More functional, concise, Ruby-idiomatic.
1106
+
1107
+ ---
1108
+
1109
+ ### 20. Logger Deprecation Warning
1110
+
1111
+ **Location:** `lib/create_ruby_app/app.rb:1`
1112
+
1113
+ **Problem:** Test output shows: "logger was loaded from the standard
1114
+ library, but will no longer be part of the default gems starting from
1115
+ Ruby 3.5.0."
1116
+
1117
+ **Impact:** Future compatibility issue, will break in Ruby 3.5+.
1118
+
1119
+ **Before:**
1120
+ ```ruby
1121
+ # In create_ruby_app.gemspec:
1122
+ s.add_development_dependency "rake", "~> 13.3.0"
1123
+ s.add_development_dependency "rspec", "~> 3.13.1"
1124
+ s.add_development_dependency "simplecov", "~> 0.22.0"
1125
+ ```
1126
+
1127
+ **After:**
1128
+ ```ruby
1129
+ # In create_ruby_app.gemspec:
1130
+ s.add_runtime_dependency "logger", "~> 1.6"
1131
+
1132
+ s.add_development_dependency "rake", "~> 13.3.0"
1133
+ s.add_development_dependency "rspec", "~> 3.13.1"
1134
+ s.add_development_dependency "simplecov", "~> 0.22.0"
1135
+ ```
1136
+
1137
+ **Benefit:** Future-proof, no warnings, follows Ruby 3.5+ requirements.
1138
+
1139
+ ---
1140
+
1141
+ ## Refactoring Opportunities
1142
+
1143
+ ### 21. Extract Action Pipeline Pattern
1144
+
1145
+ **Current:** Actions hardcoded in `App#run!`
1146
+
1147
+ **Before:**
1148
+ ```ruby
1149
+ def run!
1150
+ with_logger("Creating directories...", Actions::CreateDirectories)
1151
+ with_logger(
1152
+ "Setting Ruby implementations...",
1153
+ Actions::SetRubyImplementation
1154
+ )
1155
+ with_logger("Generating files...", Actions::GenerateFiles)
1156
+ with_logger("Making script executable...", Actions::MakeScriptExecutable)
1157
+ with_logger("Installing gems...", Actions::InstallGems)
1158
+ with_logger("Happy hacking!", ->(_) {})
1159
+ end
1160
+ ```
1161
+
1162
+ **After:**
1163
+ ```ruby
1164
+ # lib/create_ruby_app/action_pipeline.rb
1165
+ module CreateRubyApp
1166
+ class ActionPipeline
1167
+ Step = Struct.new(:message, :action, keyword_init: true)
1168
+
1169
+ def initialize(steps, logger)
1170
+ @steps = steps
1171
+ @logger = logger
1172
+ end
1173
+
1174
+ def execute(context)
1175
+ @steps.each do |step|
1176
+ @logger.info(step.message)
1177
+ step.action.call(context)
1178
+ end
1179
+ end
1180
+ end
1181
+ end
1182
+
1183
+ # In App class:
1184
+ def run!
1185
+ ActionPipeline.new(pipeline_steps, logger).execute(self)
1186
+ logger.info("Happy hacking!")
1187
+ end
1188
+
1189
+ def pipeline_steps
1190
+ [
1191
+ ActionPipeline::Step.new(
1192
+ message: "Creating directories...",
1193
+ action: Actions::CreateDirectories
1194
+ ),
1195
+ ActionPipeline::Step.new(
1196
+ message: "Setting Ruby implementations...",
1197
+ action: Actions::SetRubyImplementation
1198
+ ),
1199
+ ActionPipeline::Step.new(
1200
+ message: "Generating files...",
1201
+ action: Actions::GenerateFiles
1202
+ ),
1203
+ ActionPipeline::Step.new(
1204
+ message: "Making script executable...",
1205
+ action: Actions::MakeScriptExecutable
1206
+ ),
1207
+ ActionPipeline::Step.new(
1208
+ message: "Installing gems...",
1209
+ action: Actions::InstallGems
1210
+ )
1211
+ ]
1212
+ end
1213
+ ```
1214
+
1215
+ **Benefit:** Testable pipeline, configurable actions, follows Strategy
1216
+ pattern, Open/Closed Principle.
1217
+
1218
+ ---
1219
+
1220
+ ### 22. Introduce Value Objects
1221
+
1222
+ **Current:** Primitive strings for `name`, `version`
1223
+
1224
+ **Before:**
1225
+ ```ruby
1226
+ def initialize(name: "app", gems: [], version: nil, logger: Logger.new($stdout))
1227
+ @name = name
1228
+ @gems = gems
1229
+ @version = version
1230
+ @logger = logger
1231
+ end
1232
+ ```
1233
+
1234
+ **After:**
1235
+ ```ruby
1236
+ # lib/create_ruby_app/value_objects/app_name.rb
1237
+ module CreateRubyApp
1238
+ class AppName
1239
+ attr_reader :value
1240
+
1241
+ def initialize(value)
1242
+ @value = normalize(value)
1243
+ validate!
1244
+ end
1245
+
1246
+ def to_s
1247
+ value
1248
+ end
1249
+
1250
+ def classify
1251
+ value.split("_").map(&:capitalize).join
1252
+ end
1253
+
1254
+ private
1255
+
1256
+ def normalize(name)
1257
+ name.to_s.tr("-", "_")
1258
+ end
1259
+
1260
+ def validate!
1261
+ if value.empty?
1262
+ raise ArgumentError, "App name cannot be empty"
1263
+ end
1264
+
1265
+ unless value.match?(/\A[a-z_][a-z0-9_]*\z/)
1266
+ raise ArgumentError,
1267
+ "App name must be valid Ruby identifier: #{value}"
1268
+ end
1269
+ end
1270
+ end
1271
+ end
1272
+
1273
+ # lib/create_ruby_app/value_objects/ruby_version.rb
1274
+ module CreateRubyApp
1275
+ class RubyVersion
1276
+ attr_reader :implementation, :version
1277
+
1278
+ def initialize(version_string)
1279
+ @implementation, @version = parse(version_string)
1280
+ validate!
1281
+ end
1282
+
1283
+ def to_s
1284
+ "#{implementation}-#{version}"
1285
+ end
1286
+
1287
+ private
1288
+
1289
+ def parse(version_string)
1290
+ parts = version_string.to_s.split("-", 2)
1291
+ [parts[0], parts[1]]
1292
+ end
1293
+
1294
+ def validate!
1295
+ if implementation.nil? || version.nil?
1296
+ raise ArgumentError, "Invalid Ruby version format"
1297
+ end
1298
+ end
1299
+ end
1300
+ end
1301
+
1302
+ # Usage in App:
1303
+ def initialize(
1304
+ name: "app",
1305
+ gems: [],
1306
+ version: nil,
1307
+ logger: Logger.new($stdout)
1308
+ )
1309
+ @name = AppName.new(name)
1310
+ @gems = gems
1311
+ @version = version ? RubyVersion.new(version) : nil
1312
+ @logger = logger
1313
+ end
1314
+ ```
1315
+
1316
+ **Benefit:** Type safety, validation in one place, clearer domain model,
1317
+ follows Value Object pattern.
1318
+
1319
+ ---
1320
+
1321
+ ### 23. Extract Path Management
1322
+
1323
+ **Current:** Path strings constructed everywhere
1324
+
1325
+ **Before:**
1326
+ ```ruby
1327
+ # Scattered throughout actions:
1328
+ Pathname.new("#{app.name}/bin")
1329
+ "#{app.name}/lib/#{app.name}"
1330
+ FileUtils.chmod("+x", "#{app.name}/bin/#{app.name}")
1331
+ ```
1332
+
1333
+ **After:**
1334
+ ```ruby
1335
+ # lib/create_ruby_app/project_structure.rb
1336
+ module CreateRubyApp
1337
+ class ProjectStructure
1338
+ attr_reader :project_name
1339
+
1340
+ def initialize(project_name)
1341
+ @project_name = project_name
1342
+ end
1343
+
1344
+ def root
1345
+ @root ||= Pathname.new(project_name)
1346
+ end
1347
+
1348
+ def bin_dir
1349
+ root.join("bin")
1350
+ end
1351
+
1352
+ def bin_script
1353
+ bin_dir.join(project_name)
1354
+ end
1355
+
1356
+ def lib_dir
1357
+ root.join("lib")
1358
+ end
1359
+
1360
+ def lib_subdir
1361
+ lib_dir.join(project_name)
1362
+ end
1363
+
1364
+ def lib_main_file
1365
+ lib_dir.join("#{project_name}.rb")
1366
+ end
1367
+
1368
+ def spec_dir
1369
+ root.join("spec")
1370
+ end
1371
+
1372
+ def spec_lib_dir
1373
+ spec_dir.join("lib", project_name)
1374
+ end
1375
+
1376
+ def spec_helper
1377
+ spec_dir.join("spec_helper.rb")
1378
+ end
1379
+
1380
+ def ruby_version_file
1381
+ root.join(".ruby-version")
1382
+ end
1383
+
1384
+ def gemfile
1385
+ root.join("Gemfile")
1386
+ end
1387
+
1388
+ def directories
1389
+ [bin_dir, lib_subdir, spec_lib_dir]
1390
+ end
1391
+ end
1392
+ end
1393
+
1394
+ # Usage in App:
1395
+ def paths
1396
+ @paths ||= ProjectStructure.new(name)
1397
+ end
1398
+
1399
+ # Usage in actions:
1400
+ class CreateDirectories
1401
+ def self.call(app)
1402
+ app.paths.directories.each(&FileUtils.method(:mkdir_p))
1403
+ end
1404
+ end
1405
+ ```
1406
+
1407
+ **Benefit:** DRY, testable, single source of truth, easier to modify
1408
+ structure.
1409
+
1410
+ ---
1411
+
1412
+ ### 24. Consider Result Objects Instead of Exceptions
1413
+
1414
+ **Current:** Exceptions for control flow in `SetRubyImplementation`
1415
+
1416
+ **Before:**
1417
+ ```ruby
1418
+ def call
1419
+ return app.version if app.version
1420
+
1421
+ RUBY_IMPLEMENTATIONS.each do |ruby_implementation|
1422
+ stdout, status = fetch_ruby_implementation(ruby_implementation)
1423
+
1424
+ if status.success? && !stdout.empty?
1425
+ return transform_output_to_ruby_version(stdout)
1426
+ end
1427
+ end
1428
+
1429
+ raise NoRubyImplementationFoundError,
1430
+ "No version of Ruby is found or provided"
1431
+ end
1432
+ ```
1433
+
1434
+ **After (using dry-monads):**
1435
+ ```ruby
1436
+ require "dry/monads"
1437
+
1438
+ class SetRubyImplementation
1439
+ include Dry::Monads[:result]
1440
+
1441
+ def self.call(app)
1442
+ return Success(app.version) if app.version
1443
+
1444
+ result = DEFAULT_IMPLEMENTATIONS.lazy.map do |impl|
1445
+ detect_ruby_implementation(impl)
1446
+ end.find(&:success?)
1447
+
1448
+ result || Failure(
1449
+ :no_ruby_found,
1450
+ "No version of Ruby is found or provided"
1451
+ )
1452
+ end
1453
+
1454
+ def self.detect_ruby_implementation(implementation)
1455
+ stdout, status = fetch_ruby_implementation(implementation)
1456
+
1457
+ if status.success? && !stdout.empty?
1458
+ Success(transform_output_to_ruby_version(stdout))
1459
+ else
1460
+ Failure(:not_found, implementation)
1461
+ end
1462
+ end
1463
+
1464
+ # ... rest of methods
1465
+ end
1466
+
1467
+ # In App#run!:
1468
+ result = Actions::SetRubyImplementation.call(self)
1469
+
1470
+ result.fmap { |version| @version = version }
1471
+ .or { |error| raise NoRubyImplementationFoundError, error }
1472
+ ```
1473
+
1474
+ **Benefit:** More functional, explicit error handling, better
1475
+ performance, clearer control flow.
1476
+
1477
+ ---
1478
+
1479
+ ## Testing Improvements Needed
1480
+
1481
+ ### 25. Missing Tests
1482
+
1483
+ **Examples of missing test coverage:**
1484
+
1485
+ ```ruby
1486
+ # spec/lib/create_ruby_app/app_spec.rb - Add:
1487
+ RSpec.describe CreateRubyApp::App do
1488
+ describe "#with_logger" do
1489
+ it "logs message before executing action" do
1490
+ logger = instance_double(Logger)
1491
+ app = described_class.new(
1492
+ name: "test",
1493
+ version: "ruby-3.4.0",
1494
+ logger: logger
1495
+ )
1496
+ action = ->(_) {}
1497
+
1498
+ expect(logger).to receive(:info).with("Test message").ordered
1499
+ expect(action).to receive(:call).with(app).ordered
1500
+
1501
+ app.send(:with_logger, "Test message", action)
1502
+ end
1503
+ end
1504
+ end
1505
+
1506
+ # spec/lib/create_ruby_app/actions/create_directories_spec.rb - Add:
1507
+ RSpec.describe CreateRubyApp::Actions::CreateDirectories do
1508
+ describe ".call" do
1509
+ context "when directory creation fails due to permissions" do
1510
+ it "raises DirectoryCreationError" do
1511
+ app = instance_double(
1512
+ CreateRubyApp::App,
1513
+ name: "test_app"
1514
+ )
1515
+
1516
+ allow(FileUtils).to receive(:mkdir_p)
1517
+ .and_raise(Errno::EACCES, "Permission denied")
1518
+
1519
+ expect { described_class.call(app) }
1520
+ .to raise_error(
1521
+ CreateRubyApp::DirectoryCreationError,
1522
+ /Permission denied/
1523
+ )
1524
+ end
1525
+ end
1526
+ end
1527
+ end
1528
+ ```
1529
+
1530
+ ### 26. Test Improvements
1531
+
1532
+ **Reduce duplication in set_ruby_implementation_spec.rb:**
1533
+
1534
+ **Before:**
1535
+ ```ruby
1536
+ context "when no version if provided, but an `mruby` implementation is found" do
1537
+ let(:app) do
1538
+ instance_double(CreateRubyApp::App, name: "mruby_app", version: nil)
1539
+ end
1540
+
1541
+ it "returns the local version" do
1542
+ %w[ruby truffleruby jruby rubinius].each do |ruby_implementation|
1543
+ allow(Open3).to receive(:capture3).with(
1544
+ "#{ruby_implementation} -v"
1545
+ ).and_return(
1546
+ ["", "", instance_double(Process::Status, success?: false)]
1547
+ )
1548
+ end
1549
+
1550
+ allow(Open3).to receive(:capture3).with("mruby -v").and_return(
1551
+ ["mruby 3.3.0", "", instance_double(Process::Status, success?: true)]
1552
+ )
1553
+
1554
+ # ... more repetitive setup
1555
+ end
1556
+ end
1557
+ ```
1558
+
1559
+ **After:**
1560
+ ```ruby
1561
+ RSpec.describe CreateRubyApp::Actions::SetRubyImplementation do
1562
+ describe ".call" do
1563
+ let(:app) { instance_double(CreateRubyApp::App, version: nil) }
1564
+
1565
+ def stub_ruby_implementations(except:, found_version:)
1566
+ implementations = %w[ruby truffleruby jruby mruby rubinius]
1567
+
1568
+ implementations.each do |impl|
1569
+ if impl == except
1570
+ allow(Open3).to receive(:capture3).with("#{impl} -v")
1571
+ .and_return([
1572
+ found_version,
1573
+ "",
1574
+ instance_double(Process::Status, success?: true)
1575
+ ])
1576
+ else
1577
+ allow(Open3).to receive(:capture3).with("#{impl} -v")
1578
+ .and_return([
1579
+ "",
1580
+ "",
1581
+ instance_double(Process::Status, success?: false)
1582
+ ])
1583
+ end
1584
+ end
1585
+ end
1586
+
1587
+ context "when mruby implementation is found" do
1588
+ it "returns the mruby version" do
1589
+ stub_ruby_implementations(
1590
+ except: "mruby",
1591
+ found_version: "mruby 3.3.0"
1592
+ )
1593
+
1594
+ expect(described_class.call(app)).to eq("mruby-3.3.0")
1595
+ end
1596
+ end
1597
+
1598
+ context "when jruby implementation is found" do
1599
+ it "returns the jruby version" do
1600
+ stub_ruby_implementations(
1601
+ except: "jruby",
1602
+ found_version: "jruby 9.4.14.0"
1603
+ )
1604
+
1605
+ expect(described_class.call(app)).to eq("jruby-9.4.14.0")
1606
+ end
1607
+ end
1608
+ end
1609
+ end
1610
+ ```
1611
+
1612
+ ---
1613
+
1614
+ ## Priority Ranking (High Impact, Low Effort First)
1615
+
1616
+ 1. **Add logger gem to gemspec** (High impact, 1 line, immediate
1617
+ benefit)
1618
+ 2. **Fix system call error handling in InstallGems** (High impact, 3
1619
+ lines, prevents silent failures)
1620
+ 3. **Remove empty CreateRubyApp::CreateRubyApp class** (Low effort, 2
1621
+ lines, clean code)
1622
+ 4. **Extract gem formatting logic in GenerateFiles** (Medium effort,
1623
+ improves readability significantly)
1624
+ 5. **Make SetRubyImplementation pure** (High impact, medium effort,
1625
+ core FP principle)
1626
+ 6. **Extract path management methods to App** (Medium effort, high DRY
1627
+ improvement)
1628
+ 7. **Standardize action interface** (Medium effort, reduces boilerplate
1629
+ significantly)
1630
+ 8. **Add missing error handling and custom exceptions** (High impact,
1631
+ prevents silent failures)
1632
+ 9. **Extract action pipeline pattern** (Higher effort, major
1633
+ architectural improvement)
1634
+ 10. **Increase test coverage to 90%** (Higher effort, but critical for
1635
+ confidence)
1636
+
1637
+ ---
1638
+
1639
+ ## Summary
1640
+
1641
+ The codebase is solid but has room for improvement in:
1642
+
1643
+ - **Immutability:** Reduce mutations, make functions pure
1644
+ - **SOLID:** Better separation of concerns, dependency inversion,
1645
+ Open/Closed
1646
+ - **DRY:** Extract repeated path logic and patterns
1647
+ - **Error Handling:** Consistent, comprehensive error handling
1648
+ - **Testability:** Improve coverage and reduce coupling for easier
1649
+ testing
1650
+ - **Functional Programming:** More pure functions, less side effects
1651
+
1652
+ **Total Issues Identified:** 26
1653
+ **Estimated Effort to Address All:** Medium (2-3 days of focused work)
1654
+ **Risk Level of Changes:** Low to Medium (good test coverage enables
1655
+ safe refactoring)
1656
+
1657
+ The improvements are incremental and can be tackled independently,
1658
+ making this a manageable refactoring effort with significant quality
1659
+ gains.