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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/CLAUDE.md +74 -0
- data/CODE_REVIEW.md +1659 -0
- data/LICENSE +13 -21
- data/README.md +5 -5
- data/REFACTORING_PLAN.md +543 -0
- data/bin/create-ruby-app +1 -3
- data/lib/create_ruby_app/actions/create_directories.rb +10 -2
- data/lib/create_ruby_app/actions/generate_files.rb +7 -4
- data/lib/create_ruby_app/actions/install_gems.rb +10 -2
- data/lib/create_ruby_app/actions/make_script_executable.rb +10 -2
- data/lib/create_ruby_app/actions/set_ruby_implementation.rb +52 -0
- data/lib/create_ruby_app/app.rb +9 -8
- data/lib/create_ruby_app/cli.rb +58 -41
- data/lib/create_ruby_app/templates/Gemfile.erb +1 -3
- data/lib/create_ruby_app/templates/lib_file.erb +0 -2
- data/lib/create_ruby_app/templates/script_file.erb +0 -2
- data/lib/create_ruby_app/templates/spec_helper.erb +0 -2
- data/lib/create_ruby_app/version.rb +1 -3
- data/lib/create_ruby_app.rb +1 -3
- data/spec/integration/app_creation_spec.rb +170 -0
- data/spec/lib/create_ruby_app/actions/create_directories_spec.rb +1 -3
- data/spec/lib/create_ruby_app/actions/generate_files_spec.rb +13 -20
- data/spec/lib/create_ruby_app/actions/install_gems_spec.rb +1 -3
- data/spec/lib/create_ruby_app/actions/make_script_executable_spec.rb +1 -3
- data/spec/lib/create_ruby_app/actions/set_ruby_implementation_spec.rb +194 -0
- data/spec/lib/create_ruby_app/app_spec.rb +4 -4
- data/spec/lib/create_ruby_app/cli_spec.rb +112 -0
- data/spec/spec_helper.rb +6 -2
- metadata +52 -20
- data/lib/create_ruby_app/actions/null_action.rb +0 -9
data/LICENSE
CHANGED
@@ -1,21 +1,13 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
-
SOFTWARE.
|
1
|
+
Copyright (c) 2020 Mathias Jean Johansen
|
2
|
+
|
3
|
+
Permission to use, copy, modify, and distribute this software for any purpose
|
4
|
+
with or without fee is hereby granted, provided that the above copyright notice
|
5
|
+
and this permission notice appear in all copies.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
8
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
9
|
+
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
10
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
11
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
12
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
13
|
+
PERFORMANCE OF THIS SOFTWARE.
|
data/README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# Create Ruby App
|
2
|
-
|
2
|
+

|
3
3
|
[](http://badge.fury.io/rb/create-ruby-app)
|
4
4
|
|
5
5
|
`create-ruby-app` is an opinionated tool for scaffolding Ruby applications
|
@@ -14,7 +14,7 @@ instead, and if you are building a gem, please take a look at the `bundle gem`
|
|
14
14
|
command.
|
15
15
|
|
16
16
|
## Requirements
|
17
|
-
* Ruby (version 2.
|
17
|
+
* Ruby (version 2.7.1 or newer).
|
18
18
|
|
19
19
|
## Installation
|
20
20
|
```
|
@@ -23,12 +23,12 @@ gem install create-ruby-app
|
|
23
23
|
|
24
24
|
## Usage
|
25
25
|
```
|
26
|
-
create-ruby-app new NAME [--ruby
|
26
|
+
create-ruby-app new NAME [--ruby RUBY_VERSION] [--gems GEMS]
|
27
27
|
```
|
28
28
|
|
29
29
|
### Example
|
30
30
|
```
|
31
|
-
create-ruby-app new my-app --gems sinatra,sequel --ruby ruby-2.
|
31
|
+
create-ruby-app new my-app --gems sinatra,sequel --ruby ruby-2.7.1
|
32
32
|
```
|
33
33
|
|
34
34
|
This will generate the following project structure with
|
@@ -54,4 +54,4 @@ Once the project is generated, it will run `bundle install` so you can start
|
|
54
54
|
working.
|
55
55
|
|
56
56
|
## License
|
57
|
-
See [LICENSE](https://github.com/majjoha/create-ruby-app/blob/
|
57
|
+
See [LICENSE](https://github.com/majjoha/create-ruby-app/blob/main/LICENSE).
|
data/REFACTORING_PLAN.md
ADDED
@@ -0,0 +1,543 @@
|
|
1
|
+
# Pragmatic Refactoring Plan for create-ruby-app
|
2
|
+
|
3
|
+
## Philosophy
|
4
|
+
|
5
|
+
For a small project like this, we'll focus on:
|
6
|
+
- **High-impact, low-effort changes first**
|
7
|
+
- **Maintaining backwards compatibility**
|
8
|
+
- **Keeping it simple - avoid over-engineering**
|
9
|
+
- **Each step is independently valuable and testable**
|
10
|
+
|
11
|
+
---
|
12
|
+
|
13
|
+
## Phase 1: Quick Wins (1-2 hours)
|
14
|
+
|
15
|
+
These changes are safe, require minimal testing, and provide immediate
|
16
|
+
value.
|
17
|
+
|
18
|
+
### Step 1.1: Add logger gem dependency
|
19
|
+
**File:** `create_ruby_app.gemspec`
|
20
|
+
|
21
|
+
**Change:**
|
22
|
+
```ruby
|
23
|
+
s.add_runtime_dependency "logger", "~> 1.6"
|
24
|
+
```
|
25
|
+
|
26
|
+
**Why:** Fixes Ruby 3.5+ compatibility. Zero risk.
|
27
|
+
|
28
|
+
**Test:** Run tests, verify no warnings.
|
29
|
+
|
30
|
+
---
|
31
|
+
|
32
|
+
### Step 1.2: Remove empty class
|
33
|
+
**File:** `lib/create_ruby_app.rb`
|
34
|
+
|
35
|
+
**Change:** Delete lines 11-12 (the empty `CreateRubyApp::CreateRubyApp`
|
36
|
+
class)
|
37
|
+
|
38
|
+
**Why:** Dead code removal. Zero risk.
|
39
|
+
|
40
|
+
**Test:** Run tests, ensure nothing breaks.
|
41
|
+
|
42
|
+
---
|
43
|
+
|
44
|
+
### Step 1.3: Remove outdated gemspec date
|
45
|
+
**File:** `create_ruby_app.gemspec`
|
46
|
+
|
47
|
+
**Change:** Delete line 8 (`s.date = "2019-03-11"`)
|
48
|
+
|
49
|
+
**Why:** RubyGems sets this automatically. Zero risk.
|
50
|
+
|
51
|
+
**Test:** Build gem, verify metadata looks correct.
|
52
|
+
|
53
|
+
---
|
54
|
+
|
55
|
+
### Step 1.4: Add explanatory comment to CLI
|
56
|
+
**File:** `lib/create_ruby_app/cli.rb`
|
57
|
+
|
58
|
+
**Change:**
|
59
|
+
```ruby
|
60
|
+
module Commands
|
61
|
+
# Extend (not include) required for Dry::CLI registry pattern
|
62
|
+
extend Dry::CLI::Registry
|
63
|
+
# ...
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
**Why:** Better documentation. Zero risk.
|
68
|
+
|
69
|
+
**Test:** None needed.
|
70
|
+
|
71
|
+
---
|
72
|
+
|
73
|
+
## Phase 2: Error Handling (2-3 hours)
|
74
|
+
|
75
|
+
Add proper error handling without changing architecture.
|
76
|
+
|
77
|
+
### Step 2.1: Create centralized error classes
|
78
|
+
**New file:** `lib/create_ruby_app/errors.rb`
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
module CreateRubyApp
|
82
|
+
class Error < StandardError; end
|
83
|
+
|
84
|
+
class NoRubyImplementationFoundError < Error; end
|
85
|
+
class DirectoryCreationError < Error; end
|
86
|
+
class FileGenerationError < Error; end
|
87
|
+
class GemInstallationError < Error; end
|
88
|
+
class ScriptExecutableError < Error; end
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
**File:** `lib/create_ruby_app.rb` - Add:
|
93
|
+
```ruby
|
94
|
+
require_relative "create_ruby_app/errors"
|
95
|
+
```
|
96
|
+
|
97
|
+
**Why:** Centralized, consistent error handling.
|
98
|
+
|
99
|
+
**Test:** Ensure errors are loadable.
|
100
|
+
|
101
|
+
---
|
102
|
+
|
103
|
+
### Step 2.2: Add error handling to InstallGems
|
104
|
+
**File:** `lib/create_ruby_app/actions/install_gems.rb`
|
105
|
+
|
106
|
+
**Change:**
|
107
|
+
```ruby
|
108
|
+
def call
|
109
|
+
Dir.chdir(app.name) do
|
110
|
+
success = system("bundle", "install")
|
111
|
+
|
112
|
+
unless success
|
113
|
+
raise GemInstallationError,
|
114
|
+
"Failed to install gems. Exit code: #{$?.exitstatus}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
rescue Errno::ENOENT
|
118
|
+
raise GemInstallationError,
|
119
|
+
"Bundler not found. Run: gem install bundler"
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
**Why:** Prevent silent failures.
|
124
|
+
|
125
|
+
**Test:** Mock failing bundle install, verify error is raised.
|
126
|
+
|
127
|
+
---
|
128
|
+
|
129
|
+
### Step 2.3: Move existing error to errors.rb
|
130
|
+
**File:** `lib/create_ruby_app/actions/set_ruby_implementation.rb`
|
131
|
+
|
132
|
+
**Change:** Remove `NoRubyImplementationFoundError` class definition
|
133
|
+
(it's now in errors.rb).
|
134
|
+
|
135
|
+
**Why:** Consistency.
|
136
|
+
|
137
|
+
**Test:** Run tests, ensure error still works.
|
138
|
+
|
139
|
+
---
|
140
|
+
|
141
|
+
## Phase 3: Code Quality Improvements (2-3 hours)
|
142
|
+
|
143
|
+
Improve readability and maintainability without architectural changes.
|
144
|
+
|
145
|
+
### Step 3.1: Simplify action interface
|
146
|
+
**File:** All action files
|
147
|
+
|
148
|
+
**Pattern to apply:**
|
149
|
+
```ruby
|
150
|
+
# BEFORE:
|
151
|
+
class CreateDirectories
|
152
|
+
def initialize(app)
|
153
|
+
@app = app
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.call(app)
|
157
|
+
new(app).call
|
158
|
+
end
|
159
|
+
|
160
|
+
def call
|
161
|
+
# ... implementation
|
162
|
+
end
|
163
|
+
|
164
|
+
attr_reader :app
|
165
|
+
end
|
166
|
+
|
167
|
+
# AFTER:
|
168
|
+
class CreateDirectories
|
169
|
+
def self.call(app)
|
170
|
+
# ... implementation (access app directly)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
**Files to change:**
|
176
|
+
- `lib/create_ruby_app/actions/create_directories.rb`
|
177
|
+
- `lib/create_ruby_app/actions/make_script_executable.rb`
|
178
|
+
- `lib/create_ruby_app/actions/install_gems.rb`
|
179
|
+
|
180
|
+
**Why:** Less boilerplate, clearer intent.
|
181
|
+
|
182
|
+
**Test:** Run all tests, ensure actions still work.
|
183
|
+
|
184
|
+
**Note:** Keep `GenerateFiles` and `SetRubyImplementation` as-is for
|
185
|
+
now (they have more complex state).
|
186
|
+
|
187
|
+
---
|
188
|
+
|
189
|
+
### Step 3.2: Extract gem formatting method
|
190
|
+
**File:** `lib/create_ruby_app/actions/generate_files.rb`
|
191
|
+
|
192
|
+
**Change:**
|
193
|
+
```ruby
|
194
|
+
def gemfile
|
195
|
+
generate_file(
|
196
|
+
file: "Gemfile.erb",
|
197
|
+
locals: { gems: format_gems_for_gemfile(app.gems) }
|
198
|
+
)
|
199
|
+
end
|
200
|
+
|
201
|
+
private
|
202
|
+
|
203
|
+
def format_gems_for_gemfile(gems)
|
204
|
+
gems
|
205
|
+
.sort
|
206
|
+
.map { |gem| "gem \"#{gem}\"" }
|
207
|
+
.join("\n")
|
208
|
+
end
|
209
|
+
```
|
210
|
+
|
211
|
+
**Why:** Testable, readable.
|
212
|
+
|
213
|
+
**Test:** Test `format_gems_for_gemfile` independently.
|
214
|
+
|
215
|
+
---
|
216
|
+
|
217
|
+
### Step 3.3: Make template constants private
|
218
|
+
**File:** `lib/create_ruby_app/actions/generate_files.rb`
|
219
|
+
|
220
|
+
**Change:**
|
221
|
+
```ruby
|
222
|
+
TRIM_MODE = "<>"
|
223
|
+
TEMPLATES_DIR = "../templates"
|
224
|
+
|
225
|
+
private_constant :TRIM_MODE, :TEMPLATES_DIR
|
226
|
+
```
|
227
|
+
|
228
|
+
**Why:** Better encapsulation.
|
229
|
+
|
230
|
+
**Test:** Ensure constants still work internally.
|
231
|
+
|
232
|
+
---
|
233
|
+
|
234
|
+
### Step 3.4: Replace empty lambda with explicit method
|
235
|
+
**File:** `lib/create_ruby_app/app.rb`
|
236
|
+
|
237
|
+
**Change:**
|
238
|
+
```ruby
|
239
|
+
def run!
|
240
|
+
with_logger("Creating directories...", Actions::CreateDirectories)
|
241
|
+
with_logger(
|
242
|
+
"Setting Ruby implementations...",
|
243
|
+
Actions::SetRubyImplementation
|
244
|
+
)
|
245
|
+
with_logger("Generating files...", Actions::GenerateFiles)
|
246
|
+
with_logger("Making script executable...", Actions::MakeScriptExecutable)
|
247
|
+
with_logger("Installing gems...", Actions::InstallGems)
|
248
|
+
log_completion
|
249
|
+
end
|
250
|
+
|
251
|
+
private
|
252
|
+
|
253
|
+
# ... existing methods ...
|
254
|
+
|
255
|
+
def log_completion
|
256
|
+
logger.info("Happy hacking!")
|
257
|
+
end
|
258
|
+
```
|
259
|
+
|
260
|
+
**Why:** Clear intent, no magic.
|
261
|
+
|
262
|
+
**Test:** Run app, verify completion message still appears.
|
263
|
+
|
264
|
+
---
|
265
|
+
|
266
|
+
## Phase 4: Path Management (2-3 hours)
|
267
|
+
|
268
|
+
Centralize path logic to reduce duplication.
|
269
|
+
|
270
|
+
### Step 4.1: Add path helper methods to App
|
271
|
+
**File:** `lib/create_ruby_app/app.rb`
|
272
|
+
|
273
|
+
**Add these methods:**
|
274
|
+
```ruby
|
275
|
+
def project_root
|
276
|
+
@project_root ||= Pathname.new(name)
|
277
|
+
end
|
278
|
+
|
279
|
+
def bin_dir
|
280
|
+
project_root.join("bin")
|
281
|
+
end
|
282
|
+
|
283
|
+
def bin_script_path
|
284
|
+
bin_dir.join(name)
|
285
|
+
end
|
286
|
+
|
287
|
+
def lib_dir
|
288
|
+
project_root.join("lib")
|
289
|
+
end
|
290
|
+
|
291
|
+
def lib_subdir
|
292
|
+
lib_dir.join(name)
|
293
|
+
end
|
294
|
+
|
295
|
+
def spec_lib_dir
|
296
|
+
project_root.join("spec", "lib", name)
|
297
|
+
end
|
298
|
+
|
299
|
+
def file_path(relative_path)
|
300
|
+
project_root.join(relative_path)
|
301
|
+
end
|
302
|
+
```
|
303
|
+
|
304
|
+
**Why:** DRY, single source of truth for paths.
|
305
|
+
|
306
|
+
**Test:** Test each path method returns correct Pathname.
|
307
|
+
|
308
|
+
---
|
309
|
+
|
310
|
+
### Step 4.2: Update CreateDirectories to use path methods
|
311
|
+
**File:** `lib/create_ruby_app/actions/create_directories.rb`
|
312
|
+
|
313
|
+
**Change:**
|
314
|
+
```ruby
|
315
|
+
def self.call(app)
|
316
|
+
[
|
317
|
+
app.bin_dir,
|
318
|
+
app.lib_subdir,
|
319
|
+
app.spec_lib_dir
|
320
|
+
].each(&FileUtils.method(:mkdir_p))
|
321
|
+
end
|
322
|
+
```
|
323
|
+
|
324
|
+
**Why:** Uses centralized path logic.
|
325
|
+
|
326
|
+
**Test:** Verify directories are still created correctly.
|
327
|
+
|
328
|
+
---
|
329
|
+
|
330
|
+
### Step 4.3: Update other actions to use path methods
|
331
|
+
**Files:**
|
332
|
+
- `lib/create_ruby_app/actions/make_script_executable.rb`:
|
333
|
+
```ruby
|
334
|
+
def self.call(app)
|
335
|
+
FileUtils.chmod("+x", app.bin_script_path)
|
336
|
+
end
|
337
|
+
```
|
338
|
+
|
339
|
+
- `lib/create_ruby_app/actions/generate_files.rb`:
|
340
|
+
```ruby
|
341
|
+
def create_file(path:, content:)
|
342
|
+
File.write(app.file_path(path), content)
|
343
|
+
end
|
344
|
+
```
|
345
|
+
|
346
|
+
- `lib/create_ruby_app/actions/install_gems.rb`:
|
347
|
+
```ruby
|
348
|
+
def self.call(app)
|
349
|
+
Dir.chdir(app.project_root) do
|
350
|
+
# ... rest
|
351
|
+
end
|
352
|
+
end
|
353
|
+
```
|
354
|
+
|
355
|
+
**Why:** Consistent path handling.
|
356
|
+
|
357
|
+
**Test:** Integration test - create full app, verify structure.
|
358
|
+
|
359
|
+
---
|
360
|
+
|
361
|
+
## Phase 5: Make SetRubyImplementation Pure (1-2 hours)
|
362
|
+
|
363
|
+
Remove mutation while keeping it simple.
|
364
|
+
|
365
|
+
### Step 5.1: Change SetRubyImplementation to return version
|
366
|
+
**File:** `lib/create_ruby_app/actions/set_ruby_implementation.rb`
|
367
|
+
|
368
|
+
**Change:**
|
369
|
+
```ruby
|
370
|
+
def call
|
371
|
+
return app.version if app.version
|
372
|
+
|
373
|
+
RUBY_IMPLEMENTATIONS.each do |ruby_implementation|
|
374
|
+
stdout, status = fetch_ruby_implementation(ruby_implementation)
|
375
|
+
|
376
|
+
if status.success? && !stdout.empty?
|
377
|
+
return transform_output_to_ruby_version(stdout)
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
raise NoRubyImplementationFoundError,
|
382
|
+
"No version of Ruby is found or provided"
|
383
|
+
end
|
384
|
+
```
|
385
|
+
|
386
|
+
**Why:** Pure function, no side effects.
|
387
|
+
|
388
|
+
**Test:** Update tests to expect version string, not app object.
|
389
|
+
|
390
|
+
---
|
391
|
+
|
392
|
+
### Step 5.2: Update App#run! to handle version
|
393
|
+
**File:** `lib/create_ruby_app/app.rb`
|
394
|
+
|
395
|
+
**Change:**
|
396
|
+
```ruby
|
397
|
+
def run!
|
398
|
+
with_logger("Creating directories...", Actions::CreateDirectories)
|
399
|
+
|
400
|
+
unless @version
|
401
|
+
@version = with_logger(
|
402
|
+
"Setting Ruby implementations...",
|
403
|
+
Actions::SetRubyImplementation
|
404
|
+
)
|
405
|
+
end
|
406
|
+
|
407
|
+
with_logger("Generating files...", Actions::GenerateFiles)
|
408
|
+
with_logger("Making script executable...", Actions::MakeScriptExecutable)
|
409
|
+
with_logger("Installing gems...", Actions::InstallGems)
|
410
|
+
log_completion
|
411
|
+
end
|
412
|
+
|
413
|
+
private
|
414
|
+
|
415
|
+
def with_logger(text, action)
|
416
|
+
logger.info(text)
|
417
|
+
action.call(self)
|
418
|
+
end
|
419
|
+
```
|
420
|
+
|
421
|
+
**Why:** Explicit state management.
|
422
|
+
|
423
|
+
**Test:** Ensure version is set correctly, rest of flow works.
|
424
|
+
|
425
|
+
---
|
426
|
+
|
427
|
+
## Phase 6: Test Coverage (3-4 hours)
|
428
|
+
|
429
|
+
Bring coverage from 64.71% to 90%.
|
430
|
+
|
431
|
+
### Step 6.1: Add error handling tests
|
432
|
+
Add tests for:
|
433
|
+
- `InstallGems` failure scenarios
|
434
|
+
- `CreateDirectories` permission errors
|
435
|
+
- `SetRubyImplementation` no Ruby found (already exists)
|
436
|
+
|
437
|
+
### Step 6.2: Add integration tests
|
438
|
+
Add tests for:
|
439
|
+
- CLI.call integration
|
440
|
+
- Full app creation with various options
|
441
|
+
|
442
|
+
### Step 6.3: Extract test helpers
|
443
|
+
**File:** `spec/support/ruby_implementation_stubber.rb`
|
444
|
+
|
445
|
+
```ruby
|
446
|
+
module RubyImplementationStubber
|
447
|
+
def stub_ruby_implementations(except:, found_version:)
|
448
|
+
implementations = %w[ruby truffleruby jruby mruby rubinius]
|
449
|
+
|
450
|
+
implementations.each do |impl|
|
451
|
+
if impl == except
|
452
|
+
allow(Open3).to receive(:capture3).with("#{impl} -v")
|
453
|
+
.and_return([
|
454
|
+
found_version,
|
455
|
+
"",
|
456
|
+
instance_double(Process::Status, success?: true)
|
457
|
+
])
|
458
|
+
else
|
459
|
+
allow(Open3).to receive(:capture3).with("#{impl} -v")
|
460
|
+
.and_return([
|
461
|
+
"",
|
462
|
+
"",
|
463
|
+
instance_double(Process::Status, success?: false)
|
464
|
+
])
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
```
|
470
|
+
|
471
|
+
**File:** `spec/spec_helper.rb` - Add:
|
472
|
+
```ruby
|
473
|
+
require "support/ruby_implementation_stubber"
|
474
|
+
|
475
|
+
RSpec.configure do |config|
|
476
|
+
config.include RubyImplementationStubber
|
477
|
+
end
|
478
|
+
```
|
479
|
+
|
480
|
+
**Why:** DRY in tests.
|
481
|
+
|
482
|
+
---
|
483
|
+
|
484
|
+
## What We're NOT Doing (and Why)
|
485
|
+
|
486
|
+
### ❌ Action Pipeline Pattern
|
487
|
+
**Reason:** Over-engineering for 5 actions. Current approach is fine.
|
488
|
+
|
489
|
+
### ❌ Value Objects (AppName, RubyVersion)
|
490
|
+
**Reason:** Primitive obsession isn't a problem at this scale. String
|
491
|
+
works fine.
|
492
|
+
|
493
|
+
### ❌ Context Object / Dependency Injection
|
494
|
+
**Reason:** Actions accessing `app` directly is simple and clear for
|
495
|
+
this size.
|
496
|
+
|
497
|
+
### ❌ Result Objects / Monads
|
498
|
+
**Reason:** Exceptions work well here. Don't need dry-monads complexity.
|
499
|
+
|
500
|
+
### ❌ Separate TemplateRenderer Class
|
501
|
+
**Reason:** `GenerateFiles` is manageable as-is. Splitting adds little
|
502
|
+
value.
|
503
|
+
|
504
|
+
### ❌ Decorator Pattern for Logging
|
505
|
+
**Reason:** `with_logger` method is simple and works. No need to
|
506
|
+
complicate.
|
507
|
+
|
508
|
+
---
|
509
|
+
|
510
|
+
## Summary
|
511
|
+
|
512
|
+
**Total Time:** ~12-15 hours of focused work
|
513
|
+
**Total Changes:** ~200-300 lines of code
|
514
|
+
**Risk Level:** Low (each phase is independently tested)
|
515
|
+
|
516
|
+
### What This Achieves:
|
517
|
+
✅ Ruby 3.5+ compatibility
|
518
|
+
✅ Consistent error handling
|
519
|
+
✅ Reduced boilerplate
|
520
|
+
✅ Better path management (DRY)
|
521
|
+
✅ Pure functions (SetRubyImplementation)
|
522
|
+
✅ 90% test coverage
|
523
|
+
✅ Cleaner, more maintainable code
|
524
|
+
|
525
|
+
### What This Avoids:
|
526
|
+
❌ Over-abstraction
|
527
|
+
❌ Unnecessary complexity
|
528
|
+
❌ Breaking changes
|
529
|
+
❌ Framework-level refactoring
|
530
|
+
|
531
|
+
---
|
532
|
+
|
533
|
+
## Execution Order
|
534
|
+
|
535
|
+
1. **Phase 1** (Quick Wins) - Do first, builds confidence
|
536
|
+
2. **Phase 2** (Error Handling) - Important safety improvement
|
537
|
+
3. **Phase 3** (Code Quality) - Makes codebase nicer to work with
|
538
|
+
4. **Phase 4** (Path Management) - Biggest DRY improvement
|
539
|
+
5. **Phase 5** (Pure Functions) - Core principle improvement
|
540
|
+
6. **Phase 6** (Test Coverage) - Lock in all changes
|
541
|
+
|
542
|
+
Each phase can be done independently and committed separately. You can
|
543
|
+
stop after any phase and still have a better codebase.
|
data/bin/create-ruby-app
CHANGED
@@ -1,18 +1,26 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require "fileutils"
|
4
2
|
require "pathname"
|
5
3
|
|
6
4
|
module CreateRubyApp
|
7
5
|
module Actions
|
8
6
|
class CreateDirectories
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
9
11
|
def self.call(app)
|
12
|
+
new(app).call
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
10
16
|
[
|
11
17
|
Pathname.new("#{app.name}/bin"),
|
12
18
|
Pathname.new("#{app.name}/lib/#{app.name}"),
|
13
19
|
Pathname.new("#{app.name}/spec/lib/#{app.name}")
|
14
20
|
].each(&FileUtils.method(:mkdir_p))
|
15
21
|
end
|
22
|
+
|
23
|
+
attr_reader :app
|
16
24
|
end
|
17
25
|
end
|
18
26
|
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require "erb"
|
4
2
|
require "pathname"
|
5
3
|
|
@@ -38,7 +36,12 @@ module CreateRubyApp
|
|
38
36
|
end
|
39
37
|
|
40
38
|
def gemfile
|
41
|
-
generate_file(file: "Gemfile.erb", locals: {
|
39
|
+
generate_file(file: "Gemfile.erb", locals: {
|
40
|
+
gems: app
|
41
|
+
.gems
|
42
|
+
.sort
|
43
|
+
.map {|gem| "gem \"#{gem}\"" }.join("\n")
|
44
|
+
})
|
42
45
|
end
|
43
46
|
|
44
47
|
private
|
@@ -46,7 +49,7 @@ module CreateRubyApp
|
|
46
49
|
attr_reader :app
|
47
50
|
|
48
51
|
def generate_files
|
49
|
-
files.each {|path, content| create_file(path: path, content: content) }
|
52
|
+
files.each { |path, content| create_file(path: path, content: content) }
|
50
53
|
end
|
51
54
|
|
52
55
|
def create_file(path:, content:)
|
@@ -1,11 +1,19 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module CreateRubyApp
|
4
2
|
module Actions
|
5
3
|
class InstallGems
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
6
8
|
def self.call(app)
|
9
|
+
new(app).call
|
10
|
+
end
|
11
|
+
|
12
|
+
def call
|
7
13
|
Dir.chdir(app.name) { system("bundle", "install") }
|
8
14
|
end
|
15
|
+
|
16
|
+
attr_reader :app
|
9
17
|
end
|
10
18
|
end
|
11
19
|
end
|