exercism-rb 0.1.0 → 0.2.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 +26 -0
- data/README.md +34 -58
- data/lib/exercism/rb/cli.rb +128 -38
- data/lib/exercism/rb/command_runner.rb +12 -5
- data/lib/exercism/rb/config.rb +1 -1
- data/lib/exercism/rb/exercise.rb +109 -10
- data/lib/exercism/rb/resolver.rb +4 -4
- data/lib/exercism/rb/state.rb +5 -5
- data/lib/exercism/rb/ui.rb +4 -4
- data/lib/exercism/rb/version.rb +1 -1
- metadata +71 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 52de31694fce53072b7cff336f8b31ee600c90dc10f847b2b73ff4b22616fc6b
|
|
4
|
+
data.tar.gz: 17c72ff5d2eea013d0e7edc0f8ac5a82533e5777ed9d555e8d35d10cfff45221
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9372500b14d3b8e09a4e8bc48e54a19afb6a4eae9cd91730c70bd92974d78ab57db6c51d51bbff0a450e99e65684d7bdeb64914dc98085fb4174f05e1587dda9
|
|
7
|
+
data.tar.gz: 646c0ceebb0d862635cb4c17f10ba8aa9134437c5e512e3c9dc6e58f95d03aec96c38834b1d2eb9444c3c9d8e7e229967784d783ea491005ca0814f572af326b
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.2.0 - 2026-05-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Support for `.exercism/config.json` `files.solution` and `files.test` metadata.
|
|
15
|
+
- `--no-edit` for `xrb new` to download and save the current exercise without opening an editor.
|
|
16
|
+
- Repeatable `--file FILE` overrides for `xrb submit` and `xrb test`.
|
|
17
|
+
- Standard, Bundler Audit, Reek, RubyCritic, and opt-in SimpleCov coverage as development quality tools.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `xrb submit` now delegates default file selection to the Exercism CLI when exercise config is available.
|
|
22
|
+
- `xrb test` can run multiple configured or explicitly selected test files in order.
|
|
23
|
+
- `xrb edit` and `xrb new` now require an explicit editor through `XRB_EDITOR`, `VISUAL`, or `EDITOR` instead of assuming `nvim`.
|
|
24
|
+
- CLI status, warning, and error output now uses color without log-style labels.
|
|
25
|
+
- Release process details moved from `README.md` to `CONTRIBUTING.md`.
|
|
26
|
+
|
|
27
|
+
### Removed
|
|
28
|
+
|
|
29
|
+
- Removed the legacy source installer; RubyGems is now the only supported installation path.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- `xrb new` now explains the likely workspace mismatch when download succeeds but the expected exercise directory is missing.
|
|
34
|
+
- Command execution errors now distinguish missing commands from commands that ran and returned a non-zero exit code.
|
|
35
|
+
|
|
10
36
|
## 0.1.0 - 2026-05-06
|
|
11
37
|
|
|
12
38
|
### Added
|
data/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# exercism-rb
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/exercism-rb)
|
|
4
|
+
[](https://github.com/hvpaiva/exercism-rb/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://rubygems.org/gems/exercism-rb)
|
|
7
|
+
|
|
3
8
|
`xrb` is a small CLI that removes friction from the Exercism Ruby workflow.
|
|
4
9
|
|
|
5
10
|
It remembers the current exercise, runs commands from the right exercise directory, opens your editor, starts IRB with the solution file loaded, runs tests, and submits the solution file without requiring manual `cd` work.
|
|
@@ -30,7 +35,7 @@ gem update exercism-rb
|
|
|
30
35
|
|
|
31
36
|
- Ruby 3.2+
|
|
32
37
|
- Exercism CLI for `xrb new` and `xrb submit`
|
|
33
|
-
- An editor available on `PATH`
|
|
38
|
+
- An editor available on `PATH` and configured through `XRB_EDITOR`, `VISUAL`, or `EDITOR` for `xrb edit` and default `xrb new`
|
|
34
39
|
|
|
35
40
|
Configure the Exercism CLI separately:
|
|
36
41
|
|
|
@@ -53,9 +58,9 @@ xrb submit
|
|
|
53
58
|
```bash
|
|
54
59
|
xrb new <exercise> # download, save as current, and open the editor
|
|
55
60
|
xrb edit [exercise] # open the editor for an exercise
|
|
56
|
-
xrb test [exercise] # run
|
|
61
|
+
xrb test [exercise] # run configured or selected test files
|
|
57
62
|
xrb irb [exercise] # open irb -r ./<solution>.rb --simple-prompt
|
|
58
|
-
xrb submit [exercise] # submit the
|
|
63
|
+
xrb submit [exercise] # submit through the Exercism CLI
|
|
59
64
|
xrb use <exercise> # save a downloaded exercise as current
|
|
60
65
|
xrb current # show the current exercise
|
|
61
66
|
xrb path [exercise] # print the exercise path
|
|
@@ -69,7 +74,18 @@ Exercise resolution priority:
|
|
|
69
74
|
2. Current working directory when inside `XRB_ROOT`
|
|
70
75
|
3. Saved state from the previous `xrb new` or `xrb use`
|
|
71
76
|
|
|
72
|
-
`xrb test`
|
|
77
|
+
`xrb test` reads `.exercism/config.json` when available and runs each file listed in `files.test`. Without that config, it preserves the older fallback of requiring a single `*_test.rb` file.
|
|
78
|
+
|
|
79
|
+
`xrb submit` lets the Exercism CLI choose default solution files when `.exercism/config.json` is present. Without that config, it preserves the older fallback of requiring a single solution `.rb` file.
|
|
80
|
+
|
|
81
|
+
Both `xrb test` and `xrb submit` accept a repeatable explicit override:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
xrb test --file custom_test.rb
|
|
85
|
+
xrb submit two-fer --file two_fer.rb --file helper.rb
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Use `xrb new <exercise> --no-edit` to download and save the exercise as current without opening an editor.
|
|
73
89
|
|
|
74
90
|
## Output And Color
|
|
75
91
|
|
|
@@ -109,53 +125,19 @@ updated_at = "2026-05-05T12:00:00Z"
|
|
|
109
125
|
```bash
|
|
110
126
|
XRB_ROOT=~/exercism/ruby # exercise directory
|
|
111
127
|
XRB_TRACK=ruby # Exercism track
|
|
112
|
-
XRB_EDITOR=
|
|
128
|
+
XRB_EDITOR="code --wait" # editor used by xrb new/edit
|
|
113
129
|
XRB_STATE=~/.local/state/exercism-rb/state.toml
|
|
114
130
|
XRB_COLOR=auto # auto, always, or never
|
|
115
131
|
```
|
|
116
132
|
|
|
117
133
|
`xrb new` uses `exercism download`, which downloads into the workspace configured in the Exercism CLI. If you customize `XRB_ROOT`, configure the Exercism workspace so its track directory matches it, for example `exercism configure --workspace ~/exercism` for `XRB_ROOT=~/exercism/ruby`.
|
|
118
134
|
|
|
119
|
-
Editor commands are split with shell-like quoting, so this works:
|
|
135
|
+
Set `XRB_EDITOR`, `VISUAL`, or `EDITOR` before running `xrb edit` or `xrb new` without `--no-edit`. Editor commands are split with shell-like quoting, so this works:
|
|
120
136
|
|
|
121
137
|
```bash
|
|
122
138
|
XRB_EDITOR="code --wait" xrb edit
|
|
123
139
|
```
|
|
124
140
|
|
|
125
|
-
## Source Installer
|
|
126
|
-
|
|
127
|
-
RubyGems is the recommended installation path. The repository also keeps a source installer for users who want to install directly from `main`:
|
|
128
|
-
|
|
129
|
-
```bash
|
|
130
|
-
curl -fsSL https://raw.githubusercontent.com/hvpaiva/exercism-rb/main/install.rb | ruby
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
The installer clones or updates the repository at `~/.local/share/exercism-rb` and creates this symlink:
|
|
134
|
-
|
|
135
|
-
```text
|
|
136
|
-
~/.local/bin/xrb -> ~/.local/share/exercism-rb/bin/xrb
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
It also installs the Exercism CLI into `~/.local/bin/exercism` when `exercism` is not already available.
|
|
140
|
-
|
|
141
|
-
To skip the Exercism CLI install:
|
|
142
|
-
|
|
143
|
-
```bash
|
|
144
|
-
curl -fsSL https://raw.githubusercontent.com/hvpaiva/exercism-rb/main/install.rb | ruby - --no-exercism
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
To force-install or update the Exercism CLI:
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
curl -fsSL https://raw.githubusercontent.com/hvpaiva/exercism-rb/main/install.rb | ruby - --with-exercism
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
If `~/.local/bin/xrb` is an existing symlink, the installer replaces the symlink without deleting the old target. If it is a real file or directory, the installer refuses to replace it unless you opt in:
|
|
154
|
-
|
|
155
|
-
```bash
|
|
156
|
-
curl -fsSL https://raw.githubusercontent.com/hvpaiva/exercism-rb/main/install.rb | XRB_INSTALL_OVERWRITE=1 ruby
|
|
157
|
-
```
|
|
158
|
-
|
|
159
141
|
## Development
|
|
160
142
|
|
|
161
143
|
Install development dependencies:
|
|
@@ -176,7 +158,7 @@ Run the full verification suite:
|
|
|
176
158
|
bundle exec rake ci
|
|
177
159
|
```
|
|
178
160
|
|
|
179
|
-
The CI task checks syntax, runs tests, runs tests with Ruby warnings enabled, smoke-tests the checkout executable, builds the gem, installs it into an isolated `GEM_HOME`, and smoke-tests the installed `xrb` executable.
|
|
161
|
+
The CI task checks syntax, runs tests, runs tests with Ruby warnings enabled, runs required quality checks, smoke-tests the checkout executable, builds the gem, installs it into an isolated `GEM_HOME`, and smoke-tests the installed `xrb` executable.
|
|
180
162
|
|
|
181
163
|
Useful individual tasks:
|
|
182
164
|
|
|
@@ -184,29 +166,23 @@ Useful individual tasks:
|
|
|
184
166
|
bundle exec rake test
|
|
185
167
|
bundle exec rake syntax
|
|
186
168
|
bundle exec rake warnings
|
|
169
|
+
bundle exec rake style
|
|
170
|
+
bundle exec rake audit
|
|
171
|
+
bundle exec rake quality
|
|
172
|
+
bundle exec rake coverage
|
|
187
173
|
bundle exec rake smoke:bin
|
|
188
174
|
bundle exec rake smoke:gem
|
|
189
175
|
```
|
|
190
176
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
Publishing is automated through RubyGems Trusted Publishing and GitHub Actions. No long-lived `RUBYGEMS_AUTH_TOKEN` is required for the recommended release path.
|
|
177
|
+
Optional maintenance reports:
|
|
194
178
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
Gem name: exercism-rb
|
|
199
|
-
GitHub repository: hvpaiva/exercism-rb
|
|
200
|
-
Workflow filename: release.yml
|
|
201
|
-
Environment: release
|
|
179
|
+
```bash
|
|
180
|
+
bundle exec rake smells
|
|
181
|
+
bundle exec rake critic
|
|
202
182
|
```
|
|
203
183
|
|
|
204
|
-
|
|
184
|
+
`bundle exec rake critic` writes its report to `tmp/rubycritic/`, which is ignored by Git.
|
|
205
185
|
|
|
206
|
-
|
|
207
|
-
2. Update `CHANGELOG.md`
|
|
208
|
-
3. Run `bundle exec rake ci`
|
|
209
|
-
4. Commit the release changes
|
|
210
|
-
5. Create and push a tag that matches the version, for example `v0.1.0` for `VERSION = "0.1.0"`
|
|
186
|
+
## Release
|
|
211
187
|
|
|
212
|
-
|
|
188
|
+
Release process details are maintainer documentation and live in `CONTRIBUTING.md`.
|
data/lib/exercism/rb/cli.rb
CHANGED
|
@@ -6,6 +6,24 @@ module Exercism
|
|
|
6
6
|
module Rb
|
|
7
7
|
class CLI
|
|
8
8
|
COMMANDS = %w[new edit test irb submit use current path list clear help version].freeze
|
|
9
|
+
COMMAND_METHODS = {
|
|
10
|
+
"new" => :new_command,
|
|
11
|
+
"edit" => :edit_command,
|
|
12
|
+
"test" => :test_command,
|
|
13
|
+
"irb" => :irb_command,
|
|
14
|
+
"submit" => :submit_command,
|
|
15
|
+
"use" => :use_command,
|
|
16
|
+
"current" => :current_command,
|
|
17
|
+
"path" => :path_command,
|
|
18
|
+
"list" => :list_command,
|
|
19
|
+
"clear" => :clear_command,
|
|
20
|
+
"help" => :help_command,
|
|
21
|
+
"-h" => :help_command,
|
|
22
|
+
"--help" => :help_command,
|
|
23
|
+
"version" => :version_command,
|
|
24
|
+
"-v" => :version_command,
|
|
25
|
+
"--version" => :version_command
|
|
26
|
+
}.freeze
|
|
9
27
|
|
|
10
28
|
def self.start(argv, out: $stdout, err: $stderr)
|
|
11
29
|
new(argv, out: out, err: err).start
|
|
@@ -23,27 +41,13 @@ module Exercism
|
|
|
23
41
|
|
|
24
42
|
def start
|
|
25
43
|
command = @argv.shift || "help"
|
|
44
|
+
handler = COMMAND_METHODS.fetch(command) { raise Error, "Unknown command: #{command}. Use `xrb help`." }
|
|
26
45
|
|
|
27
|
-
|
|
28
|
-
when "new" then new_command
|
|
29
|
-
when "edit" then edit_command
|
|
30
|
-
when "test" then test_command
|
|
31
|
-
when "irb" then irb_command
|
|
32
|
-
when "submit" then submit_command
|
|
33
|
-
when "use" then use_command
|
|
34
|
-
when "current" then current_command
|
|
35
|
-
when "path" then path_command
|
|
36
|
-
when "list" then list_command
|
|
37
|
-
when "clear" then clear_command
|
|
38
|
-
when "help", "-h", "--help" then help_command
|
|
39
|
-
when "version", "-v", "--version" then version_command
|
|
40
|
-
else
|
|
41
|
-
raise Error, "Unknown command: #{command}. Use `xrb help`."
|
|
42
|
-
end
|
|
46
|
+
__send__(handler)
|
|
43
47
|
|
|
44
48
|
0
|
|
45
|
-
rescue Error =>
|
|
46
|
-
@ui.error(
|
|
49
|
+
rescue Error => error
|
|
50
|
+
@ui.error(error.message)
|
|
47
51
|
1
|
|
48
52
|
rescue Interrupt
|
|
49
53
|
@ui.error("Interrupted.")
|
|
@@ -53,14 +57,15 @@ module Exercism
|
|
|
53
57
|
private
|
|
54
58
|
|
|
55
59
|
def new_command
|
|
60
|
+
skip_editor = extract_no_edit_option!
|
|
56
61
|
exercise = exercise_from_required_arg("new <exercise>", require_existing: false)
|
|
57
62
|
|
|
58
63
|
@ui.info("Downloading #{@ui.highlight(exercise.slug)}...")
|
|
59
64
|
@runner.run("exercism", "download", "--track=#{exercise.track}", "--exercise=#{exercise.slug}")
|
|
60
|
-
exercise
|
|
65
|
+
ensure_download_created_exercise!(exercise)
|
|
61
66
|
save_current(exercise)
|
|
62
67
|
@ui.key_value("Path", @ui.path(exercise.path))
|
|
63
|
-
edit_exercise(exercise)
|
|
68
|
+
edit_exercise(exercise) unless skip_editor
|
|
64
69
|
end
|
|
65
70
|
|
|
66
71
|
def edit_command
|
|
@@ -68,11 +73,15 @@ module Exercism
|
|
|
68
73
|
end
|
|
69
74
|
|
|
70
75
|
def test_command
|
|
76
|
+
files = extract_file_options!
|
|
71
77
|
exercise = @resolver.resolve(optional_arg)
|
|
72
|
-
|
|
78
|
+
test_files = files.empty? ? exercise.test_files(ambiguity_hint: "Use --file FILE to choose test files explicitly.") : files
|
|
79
|
+
ensure_test_files_exist!(exercise, test_files)
|
|
73
80
|
|
|
74
81
|
@ui.info("Testing #{@ui.highlight(exercise.slug)}...")
|
|
75
|
-
|
|
82
|
+
test_files.each do |test_file|
|
|
83
|
+
@runner.run("ruby", "-r", "minitest/pride", test_file, chdir: exercise.path)
|
|
84
|
+
end
|
|
76
85
|
end
|
|
77
86
|
|
|
78
87
|
def irb_command
|
|
@@ -84,11 +93,16 @@ module Exercism
|
|
|
84
93
|
end
|
|
85
94
|
|
|
86
95
|
def submit_command
|
|
96
|
+
files = extract_file_options!
|
|
87
97
|
exercise = @resolver.resolve(optional_arg)
|
|
88
|
-
solution_file = exercise.solution_file
|
|
89
98
|
|
|
90
99
|
@ui.info("Submitting #{@ui.highlight(exercise.slug)}...")
|
|
91
|
-
|
|
100
|
+
if files.empty? && exercise.exercism_config?
|
|
101
|
+
@runner.run("exercism", "submit", chdir: exercise.path)
|
|
102
|
+
else
|
|
103
|
+
files = [exercise.solution_file(ambiguity_hint: "Use --file FILE to choose files explicitly.")] if files.empty?
|
|
104
|
+
@runner.run("exercism", "submit", *files, chdir: exercise.path)
|
|
105
|
+
end
|
|
92
106
|
end
|
|
93
107
|
|
|
94
108
|
def use_command
|
|
@@ -121,8 +135,8 @@ module Exercism
|
|
|
121
135
|
|
|
122
136
|
current = @state.load["exercise"]
|
|
123
137
|
exercises = Dir.children(@root)
|
|
124
|
-
|
|
125
|
-
|
|
138
|
+
.select { |name| File.directory?(File.join(@root, name)) && !name.start_with?(".") }
|
|
139
|
+
.sort
|
|
126
140
|
|
|
127
141
|
if exercises.empty?
|
|
128
142
|
@ui.warn("No exercises downloaded in #{@root}.")
|
|
@@ -131,9 +145,9 @@ module Exercism
|
|
|
131
145
|
|
|
132
146
|
@ui.section("Downloaded exercises")
|
|
133
147
|
exercises.each do |slug|
|
|
134
|
-
marker = slug == current ? "*" : " "
|
|
135
|
-
label = slug == current ? @ui.highlight(slug) : slug
|
|
136
|
-
suffix = slug == current ? " #{@ui.muted(
|
|
148
|
+
marker = (slug == current) ? "*" : " "
|
|
149
|
+
label = (slug == current) ? @ui.highlight(slug) : slug
|
|
150
|
+
suffix = (slug == current) ? " #{@ui.muted("current")}" : ""
|
|
137
151
|
@ui.say("#{marker} #{label}#{suffix}")
|
|
138
152
|
end
|
|
139
153
|
end
|
|
@@ -145,27 +159,31 @@ module Exercism
|
|
|
145
159
|
|
|
146
160
|
def help_command
|
|
147
161
|
@ui.say(<<~HELP)
|
|
148
|
-
#{@ui.bold(
|
|
162
|
+
#{@ui.bold("xrb")} - Exercism Ruby helper
|
|
149
163
|
|
|
150
164
|
Usage:
|
|
151
165
|
xrb new <exercise> download, save as current, and open the editor
|
|
152
166
|
xrb edit [exercise] open the editor for an exercise
|
|
153
|
-
xrb test [exercise] run
|
|
167
|
+
xrb test [exercise] run exercise tests with minitest/pride
|
|
154
168
|
xrb irb [exercise] open irb -r ./<solution>.rb --simple-prompt
|
|
155
|
-
xrb submit [exercise] submit the solution
|
|
169
|
+
xrb submit [exercise] submit the exercise solution
|
|
156
170
|
xrb use <exercise> save a downloaded exercise as current
|
|
157
171
|
xrb current show the current exercise
|
|
158
172
|
xrb path [exercise] print the exercise path
|
|
159
173
|
xrb list list downloaded exercises
|
|
160
174
|
xrb clear clear saved state
|
|
161
175
|
|
|
176
|
+
Options:
|
|
177
|
+
--no-edit skip opening the editor after xrb new
|
|
178
|
+
--file FILE test or submit an explicit file; may be repeated
|
|
179
|
+
|
|
162
180
|
State:
|
|
163
181
|
#{@state.path}
|
|
164
182
|
|
|
165
183
|
Environment:
|
|
166
184
|
XRB_ROOT exercise directory (current: #{@root})
|
|
167
185
|
XRB_TRACK Exercism track (current: #{@track})
|
|
168
|
-
XRB_EDITOR editor used by xrb edit/new
|
|
186
|
+
XRB_EDITOR editor used by xrb edit/new
|
|
169
187
|
XRB_STATE TOML state file
|
|
170
188
|
XRB_COLOR color output: auto, always, or never
|
|
171
189
|
HELP
|
|
@@ -185,10 +203,82 @@ module Exercism
|
|
|
185
203
|
@runner.run(*editor_args, target, chdir: exercise.path)
|
|
186
204
|
end
|
|
187
205
|
|
|
206
|
+
def ensure_download_created_exercise!(exercise)
|
|
207
|
+
return if exercise.exists?
|
|
208
|
+
|
|
209
|
+
raise Error, <<~MESSAGE.chomp
|
|
210
|
+
Download completed, but xrb could not find the expected exercise directory:
|
|
211
|
+
#{exercise.path}
|
|
212
|
+
|
|
213
|
+
The Exercism CLI probably downloaded to another workspace. Configure it to match XRB_ROOT:
|
|
214
|
+
exercism configure --workspace #{File.dirname(@root)}
|
|
215
|
+
MESSAGE
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def ensure_test_files_exist!(exercise, test_files)
|
|
219
|
+
test_files.each do |file|
|
|
220
|
+
next if File.file?(File.absolute_path(file, exercise.path))
|
|
221
|
+
|
|
222
|
+
raise Error, "Test file not found: #{file}"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def extract_file_options!
|
|
227
|
+
files = []
|
|
228
|
+
remaining = []
|
|
229
|
+
|
|
230
|
+
until @argv.empty?
|
|
231
|
+
arg = @argv.shift
|
|
232
|
+
|
|
233
|
+
case arg
|
|
234
|
+
when "--file"
|
|
235
|
+
value = @argv.shift
|
|
236
|
+
raise Error, "Missing value for --file" if blank?(value)
|
|
237
|
+
|
|
238
|
+
files << value
|
|
239
|
+
when /\A--file=(.*)\z/
|
|
240
|
+
value = Regexp.last_match(1)
|
|
241
|
+
raise Error, "Missing value for --file" if blank?(value)
|
|
242
|
+
|
|
243
|
+
files << value
|
|
244
|
+
else
|
|
245
|
+
raise Error, "Unknown option: #{arg}" if arg.start_with?("-")
|
|
246
|
+
|
|
247
|
+
remaining << arg
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
@argv = remaining
|
|
252
|
+
files
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def extract_no_edit_option!
|
|
256
|
+
skip_editor = false
|
|
257
|
+
remaining = []
|
|
258
|
+
|
|
259
|
+
until @argv.empty?
|
|
260
|
+
arg = @argv.shift
|
|
261
|
+
|
|
262
|
+
if arg == "--no-edit"
|
|
263
|
+
skip_editor = true
|
|
264
|
+
else
|
|
265
|
+
raise Error, "Unknown option: #{arg}" if arg.start_with?("-")
|
|
266
|
+
|
|
267
|
+
remaining << arg
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
@argv = remaining
|
|
272
|
+
skip_editor
|
|
273
|
+
end
|
|
274
|
+
|
|
188
275
|
def editor_args_from_config
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
276
|
+
editor = Config.editor
|
|
277
|
+
raise Error, "No editor configured. Set XRB_EDITOR, VISUAL, or EDITOR." if blank?(editor)
|
|
278
|
+
|
|
279
|
+
Shellwords.split(editor)
|
|
280
|
+
rescue ArgumentError => error
|
|
281
|
+
raise Error, "Invalid editor in XRB_EDITOR/VISUAL/EDITOR: #{error.message}"
|
|
192
282
|
end
|
|
193
283
|
|
|
194
284
|
def ensure_editor_available!(command, chdir:)
|
|
@@ -229,7 +319,7 @@ module Exercism
|
|
|
229
319
|
def exercise_from_required_arg(usage, require_existing: true)
|
|
230
320
|
slug = @argv.shift
|
|
231
321
|
raise Error, "Usage: xrb #{usage}" if blank?(slug)
|
|
232
|
-
raise Error, "Too many arguments: #{@argv.join(
|
|
322
|
+
raise Error, "Too many arguments: #{@argv.join(" ")}" unless @argv.empty?
|
|
233
323
|
|
|
234
324
|
Exercise.new(slug: slug, track: @track, root: @root).tap do |exercise|
|
|
235
325
|
exercise.ensure_exists! if require_existing
|
|
@@ -238,7 +328,7 @@ module Exercism
|
|
|
238
328
|
|
|
239
329
|
def optional_arg
|
|
240
330
|
slug = @argv.shift
|
|
241
|
-
raise Error, "Too many arguments: #{@argv.join(
|
|
331
|
+
raise Error, "Too many arguments: #{@argv.join(" ")}" unless @argv.empty?
|
|
242
332
|
|
|
243
333
|
slug
|
|
244
334
|
end
|
|
@@ -15,12 +15,19 @@ module Exercism
|
|
|
15
15
|
@ui.command("$ #{printable}")
|
|
16
16
|
|
|
17
17
|
ok = if chdir
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
Dir.chdir(chdir) { system(*args) }
|
|
19
|
+
else
|
|
20
|
+
system(*args)
|
|
21
|
+
end
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
case ok
|
|
24
|
+
when true
|
|
25
|
+
true
|
|
26
|
+
when nil
|
|
27
|
+
raise Error, "Command not found: #{args.first}. Install it or ensure it is on PATH."
|
|
28
|
+
else
|
|
29
|
+
raise Error, "Command failed: #{Shellwords.join(args)}"
|
|
30
|
+
end
|
|
24
31
|
|
|
25
32
|
true
|
|
26
33
|
end
|
data/lib/exercism/rb/config.rb
CHANGED
data/lib/exercism/rb/exercise.rb
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
module Exercism
|
|
4
6
|
module Rb
|
|
5
7
|
class Exercise
|
|
6
8
|
SLUG_PATTERN = /\A[a-z0-9][a-z0-9-]*\z/
|
|
9
|
+
FILE_KINDS = {
|
|
10
|
+
"test" => {config: "test file", fallback: "test file (*_test.rb)"},
|
|
11
|
+
"solution" => {config: "solution file", fallback: "solution file (.rb)"}
|
|
12
|
+
}.freeze
|
|
7
13
|
|
|
8
14
|
attr_reader :slug, :track, :root, :path
|
|
9
15
|
|
|
@@ -16,6 +22,7 @@ module Exercism
|
|
|
16
22
|
@track = track
|
|
17
23
|
@root = File.expand_path(root)
|
|
18
24
|
@path = File.expand_path(path || File.join(@root, @slug))
|
|
25
|
+
@exercism_config = nil
|
|
19
26
|
end
|
|
20
27
|
|
|
21
28
|
def exists?
|
|
@@ -28,32 +35,124 @@ module Exercism
|
|
|
28
35
|
raise Error, "Exercise not found: #{@path}"
|
|
29
36
|
end
|
|
30
37
|
|
|
31
|
-
def
|
|
32
|
-
|
|
33
|
-
pick_one(files_matching { |name| name.end_with?("_test.rb") }, kind: "test file (*_test.rb)")
|
|
38
|
+
def exercism_config?
|
|
39
|
+
File.file?(config_path)
|
|
34
40
|
end
|
|
35
41
|
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
def test_files(ambiguity_hint: nil)
|
|
43
|
+
exercise_files("test", ambiguity_hint: ambiguity_hint) do
|
|
44
|
+
fallback_test_files
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def solution_files(ambiguity_hint: nil)
|
|
49
|
+
exercise_files("solution", ambiguity_hint: ambiguity_hint) do
|
|
50
|
+
fallback_solution_files
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_file(ambiguity_hint: nil)
|
|
55
|
+
pick_one(test_files(ambiguity_hint: ambiguity_hint), kind: "test file (*_test.rb)", ambiguity_hint: ambiguity_hint)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def solution_file(ambiguity_hint: nil)
|
|
59
|
+
pick_one(solution_files(ambiguity_hint: ambiguity_hint), kind: "solution file (.rb)", ambiguity_hint: ambiguity_hint)
|
|
39
60
|
end
|
|
40
61
|
|
|
41
62
|
private
|
|
42
63
|
|
|
64
|
+
def config_path
|
|
65
|
+
File.join(@path, ".exercism", "config.json")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def exercise_files(config_name, ambiguity_hint:)
|
|
69
|
+
ensure_exists!
|
|
70
|
+
|
|
71
|
+
kinds = FILE_KINDS.fetch(config_name)
|
|
72
|
+
configured_files(config_name, kind: kinds.fetch(:config)) || [pick_one(yield, kind: kinds.fetch(:fallback), ambiguity_hint: ambiguity_hint)]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def exercism_config
|
|
76
|
+
return nil unless exercism_config?
|
|
77
|
+
return @exercism_config unless @exercism_config.nil?
|
|
78
|
+
|
|
79
|
+
config = JSON.parse(File.read(config_path))
|
|
80
|
+
raise_invalid_config("root must be an object") unless config.is_a?(Hash)
|
|
81
|
+
|
|
82
|
+
@exercism_config = config
|
|
83
|
+
rescue JSON::ParserError => error
|
|
84
|
+
raise Error, "Invalid Exercism config: #{config_path}: #{error.message}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def configured_files(name, kind:)
|
|
88
|
+
config = exercism_config
|
|
89
|
+
return nil if config.nil?
|
|
90
|
+
|
|
91
|
+
values = configured_file_values(config, name)
|
|
92
|
+
return nil if values.nil?
|
|
93
|
+
|
|
94
|
+
ensure_configured_files_exist(values, kind: kind)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def configured_file_values(config, name)
|
|
98
|
+
files = config.fetch("files", nil)
|
|
99
|
+
return nil if files.nil?
|
|
100
|
+
raise_invalid_config("files must be an object") unless files.is_a?(Hash)
|
|
101
|
+
return nil unless files.key?(name)
|
|
102
|
+
|
|
103
|
+
values = files.fetch(name)
|
|
104
|
+
validate_configured_file_list(name, values)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def validate_configured_file_list(name, values)
|
|
108
|
+
raise_invalid_config("files.#{name} must be an array") unless values.is_a?(Array)
|
|
109
|
+
raise_invalid_config("files.#{name} must not be empty") if values.empty?
|
|
110
|
+
|
|
111
|
+
values.each do |file|
|
|
112
|
+
raise_invalid_config("files.#{name} contains an empty path") unless file.is_a?(String) && !file.strip.empty?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
values
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def ensure_configured_files_exist(files, kind:)
|
|
119
|
+
files.each do |file|
|
|
120
|
+
next if File.file?(File.absolute_path(file, @path))
|
|
121
|
+
|
|
122
|
+
raise Error, "Configured #{kind} not found: #{file}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
files
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def raise_invalid_config(message)
|
|
129
|
+
raise Error, "Invalid Exercism config: #{config_path}: #{message}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def fallback_test_files
|
|
133
|
+
files_matching { |name| name.end_with?("_test.rb") }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def fallback_solution_files
|
|
137
|
+
files_matching { |name| name.end_with?(".rb") && !name.end_with?("_test.rb") }
|
|
138
|
+
end
|
|
139
|
+
|
|
43
140
|
def files_matching
|
|
44
141
|
Dir.children(@path)
|
|
45
|
-
|
|
46
|
-
|
|
142
|
+
.select { |name| File.file?(File.join(@path, name)) && yield(name) }
|
|
143
|
+
.sort
|
|
47
144
|
end
|
|
48
145
|
|
|
49
|
-
def pick_one(files, kind:)
|
|
146
|
+
def pick_one(files, kind:, ambiguity_hint: nil)
|
|
50
147
|
case files.length
|
|
51
148
|
when 0
|
|
52
149
|
raise Error, "Could not find #{kind} in #{@path}"
|
|
53
150
|
when 1
|
|
54
151
|
files.first
|
|
55
152
|
else
|
|
56
|
-
|
|
153
|
+
message = "Found more than one #{kind} in #{@path}: #{files.join(", ")}"
|
|
154
|
+
message = "#{message}. #{ambiguity_hint}" if ambiguity_hint
|
|
155
|
+
raise Error, message
|
|
57
156
|
end
|
|
58
157
|
end
|
|
59
158
|
end
|
data/lib/exercism/rb/resolver.rb
CHANGED
|
@@ -11,10 +11,10 @@ module Exercism
|
|
|
11
11
|
|
|
12
12
|
def resolve(slug = nil, require_existing: true)
|
|
13
13
|
exercise = if present?(slug)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
Exercise.new(slug: slug, track: @track, root: @root)
|
|
15
|
+
else
|
|
16
|
+
from_current_directory || from_state
|
|
17
|
+
end
|
|
18
18
|
|
|
19
19
|
exercise.ensure_exists! if require_existing
|
|
20
20
|
exercise
|
data/lib/exercism/rb/state.rb
CHANGED
|
@@ -93,11 +93,11 @@ module Exercism
|
|
|
93
93
|
|
|
94
94
|
def quote(value)
|
|
95
95
|
escaped = value.to_s
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
.gsub("\\", "\\\\")
|
|
97
|
+
.gsub('"', '\\"')
|
|
98
|
+
.gsub("\n", "\\n")
|
|
99
|
+
.gsub("\r", "\\r")
|
|
100
|
+
.gsub("\t", "\\t")
|
|
101
101
|
|
|
102
102
|
%("#{escaped}")
|
|
103
103
|
end
|
data/lib/exercism/rb/ui.rb
CHANGED
|
@@ -50,19 +50,19 @@ module Exercism
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
def info(message)
|
|
53
|
-
@out.puts(
|
|
53
|
+
@out.puts(paint(message, :blue))
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def success(message)
|
|
57
|
-
@out.puts(
|
|
57
|
+
@out.puts(paint(message, :green))
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def warn(message)
|
|
61
|
-
@err.puts(
|
|
61
|
+
@err.puts(paint(message, :yellow))
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def error(message)
|
|
65
|
-
@err.puts(
|
|
65
|
+
@err.puts(paint(message, :red))
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def command(message)
|
data/lib/exercism/rb/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: exercism-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- hvpaiva
|
|
@@ -10,6 +10,20 @@ bindir: bin
|
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 2026-05-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler-audit
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.9'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.9'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: minitest
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -38,6 +52,62 @@ dependencies:
|
|
|
38
52
|
- - "~>"
|
|
39
53
|
- !ruby/object:Gem::Version
|
|
40
54
|
version: '13.2'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: reek
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rubycritic
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: simplecov
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0.22'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0.22'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: standard
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '1.40'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '1.40'
|
|
41
111
|
description: A small command-line helper that keeps Exercism Ruby exercises easy to
|
|
42
112
|
open, test, inspect, and submit.
|
|
43
113
|
email:
|