diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index 2b1788677..000000000
--- a/.editorconfig
+++ /dev/null
@@ -1,10 +0,0 @@
-root = true
-
-[*]
-indent_style = space
-indent_size = 4
-charset = utf-8
-trim_trailing_whitespace = true
-insert_final_newline = true
-end_of_line = lf
-
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index d8a06e2ad..7efc36b87 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -4,7 +4,7 @@ name: CI
on:
push:
branches:
- - main
+ - main
pull_request:
workflow_dispatch:
schedule:
@@ -12,48 +12,11 @@ on:
- cron: "0 0 * * 0"
jobs:
- ensure-conventions:
- name: Ensure conventions are followed
- runs-on: ubuntu-latest
-
- steps:
- # Checks out a copy of your repository on the ubuntu-latest machine
- - name: Checkout code
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
-
- - name: Ensure tool names are snake cased
- run: ./bin/lint_tool_file_names.sh
-
- - name: Ensure src/lib.rs files exist
- run: ./_test/ensure_lib_src_rs_exist.sh
-
- - name: Count ignores
- run: ./_test/count_ignores.sh
-
- - name: Check UUIDs
- run: ./_test/check_uuids.sh
-
- - name: Verify exercise difficulties
- run: ./_test/verify_exercise_difficulties.sh
-
- - name: Check exercises for authors
- run: ./_test/check_exercises_for_authors.sh
-
- - name: Ensure relevant files do not have trailing whitespace
- run: ./bin/lint_trailing_spaces.sh
-
configlet:
name: configlet lint
runs-on: ubuntu-latest
steps:
- # Checks out default branch locally so that it is available to the scripts.
- - name: Checkout main
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
- with:
- ref: main
-
- # Checks out a copy of your repository on the ubuntu-latest machine
- name: Checkout code
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
@@ -68,10 +31,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- - name: Checkout main
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
- with:
- ref: main
- name: Checkout code
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
@@ -80,7 +39,7 @@ jobs:
# stolen from https://raw.githubusercontent.com/exercism/github-actions/main/.github/workflows/shellcheck.yml
shellcheck:
- name: shellcheck internal tooling lint
+ name: Run shellcheck on scripts
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -88,27 +47,16 @@ jobs:
- name: Run shellcheck
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # v2.0.0
- env:
- SHELLCHECK_OPTS: -x -s bash -e SC2001 --norc
compilation:
name: Check compilation
runs-on: ubuntu-latest
strategy:
- # Allows running the job multiple times with different configurations
matrix:
rust: ["stable", "beta"]
- deny_warnings: ['', '1']
steps:
- # Checks out main locally so that it is available to the scripts.
- - name: Checkout main
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
- with:
- ref: main
-
- # Checks out a copy of your repository on the ubuntu-latest machine
- name: Checkout code
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
@@ -117,19 +65,31 @@ jobs:
with:
toolchain: ${{ matrix.rust }}
- # run scripts as steps
- name: Check exercises
env:
- DENYWARNINGS: ${{ matrix.deny_warnings }}
- run: ./_test/check_exercises.sh
- continue-on-error: ${{ matrix.rust == 'beta' && matrix.deny_warnings == '1' }}
+ DENYWARNINGS: "1"
+ run: ./bin/check_exercises.sh
- name: Ensure stubs compile
env:
- DENYWARNINGS: ${{ matrix.deny_warnings }}
- run: ./_test/ensure_stubs_compile.sh
- continue-on-error: ${{ matrix.rust == 'beta' && matrix.deny_warnings == '1' }}
+ DENYWARNINGS: "1"
+ run: ./bin/ensure_stubs_compile.sh
+
+ tests:
+ name: Run repository tests
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+
+ - name: Setup toolchain
+ uses: dtolnay/rust-toolchain@0e66bd3e6b38ec0ad5312288c83e47c143e6b09e
+ with:
+ toolchain: stable
+ - name: Run tests
+ run: cd rust-tooling && cargo test
rustformat:
name: Check Rust Formatting
@@ -144,11 +104,8 @@ jobs:
with:
toolchain: stable
- - name: Rust Format Version
- run: rustfmt --version
-
- name: Format
- run: bin/format_exercises
+ run: ./bin/format_exercises.sh
- name: Diff
run: |
@@ -166,11 +123,6 @@ jobs:
rust: ["stable", "beta"]
steps:
- - name: Checkout main
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
- with:
- ref: main
-
- name: Checkout code
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
@@ -179,20 +131,15 @@ jobs:
with:
toolchain: ${{ matrix.rust }}
- # Clippy already installed on Stable, but not Beta.
- # So, we must install here.
- - name: Install Clippy
- run: rustup component add clippy
-
- name: Clippy tests
env:
CLIPPY: true
- run: ./_test/check_exercises.sh
+ run: ./bin/check_exercises.sh
- name: Clippy stubs
env:
CLIPPY: true
- run: ./_test/ensure_stubs_compile.sh
+ run: ./bin/ensure_stubs_compile.sh
nightly-compilation:
name: Check exercises on nightly (benchmark enabled)
@@ -200,13 +147,6 @@ jobs:
continue-on-error: true # It's okay if the nightly job fails
steps:
- # Checks out main locally so that it is available to the scripts.
- - name: Checkout main
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
- with:
- ref: main
-
- # Checks out a copy of your repository on the ubuntu-latest machine
- name: Checkout code
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
@@ -217,5 +157,5 @@ jobs:
- name: Check exercises
env:
- BENCHMARK: '1'
- run: ./_test/check_exercises.sh
+ BENCHMARK: "1"
+ run: ./bin/check_exercises.sh
diff --git a/.gitignore b/.gitignore
index edfd68168..edcc454ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,13 +3,7 @@
.DS_Store
**/target
tmp
-bin/configlet
-bin/configlet.exe
-bin/exercise
-bin/exercise.exe
-bin/generator-utils/ngram
-bin/generator-utils/escape_double_quotes
+/bin/configlet
exercises/*/*/Cargo.lock
exercises/*/*/clippy.log
-canonical_data.json
.vscode
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..bf863ee5b
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "problem-specifications"]
+ path = problem-specifications
+ url = git@github.com:exercism/problem-specifications
diff --git a/README.md b/README.md
index 31a9c76eb..6487c01f5 100644
--- a/README.md
+++ b/README.md
@@ -1,106 +1,87 @@
-# Exercism Rust Track
+
-[![CI](https://github.com/exercism/rust/workflows/CI/badge.svg?branch=main)](https://github.com/exercism/rust/actions?query=workflow%3ACI+branch%3Amain)
+
+
Exercism Rust Track
-Exercism exercises in Rust
+ [![Discourse topics](https://img.shields.io/discourse/topics?color=8A08E6&label=Connect%20&labelColor=FFDF58&logo=Discourse&logoColor=8A08E6&server=https%3A%2F%2Fforum.exercism.org&style=social)](https://forum.exercism.org)
+ [![Exercism_III](https://img.shields.io/badge/PAUSED-C73D4E?labelColor=3D454D&label=Contributions)](https://exercism.org/blog/freeing-our-maintainers)
+ [![CI](https://github.com/exercism/rust/workflows/CI/badge.svg?branch=main)](https://github.com/exercism/rust/actions?query=workflow%3ACI+branch%3Amain)
-## Contributing
+
-Check out our [contributor documentation](docs/CONTRIBUTING.md).
+Hi. ππ½ π **We are happy you are here.** π π
-## Exercise Tests
+
-At the most basic level, Exercism is all about the tests. You can read more about how we think about test suites in [the Exercism documentation](https://github.com/exercism/legacy-docs/blob/main/language-tracks/exercises/anatomy/test-suites.md).
+**`exercism/rust`** is one of many programming language tracks on [exercism(dot)org][exercism-website].
+This repo holds all the instructions, tests, code, & support files for Rust _exercises_ currently under development or implemented & available for students.
-Test files should use the following format:
+Some Exercism language tracks have a **syllabus** which is meant to teach the language step-by-step.
+The Rust track's syllabus is a work in progress and it's not activated yet.
+All exercises presented to students are **practice exercises**.
+Students are exepcted to learn the language themselves, for example with the [official book][the-rust-programming-language], and practice with our exercises.
-```
-extern crate exercise_name;
+
-use exercise_name::*;
+
+
+
+
+
-#[test]
-fn test_descriptive_name() {
- assert_eq!(exercise_function(1), 1);
-}
+ππ Please take a moment to read our [Code of Conduct][exercism-code-of-conduct] ππ
+It might also be helpful to look at [Being a Good Community Member][being-a-good-community-member] & [The words that we use][the-words-that-we-use].
+Some defined roles in our community: [Contributors][exercism-contributors] **|** [Mentors][exercism-mentors] **|** [Maintainers][exercism-track-maintainers] **|** [Admins][exercism-admins]
-#[test]
-#[ignore]
-fn test_second_and_past_tests_ignored() {
- assert_ne!(exercise_function(1), 2);
-}
-```
+
-## Opening an Issue
+
+
-If you plan to make significant or breaking changes, please open an issue so we can discuss it first. If this is a discussion that is relevant to more than just the Rust track, please open an issue in [exercism/discussions](https://github.com/exercism/discussions/issues).
+We π π our community.
+**`But our maintainers are not accepting community contributions at this time.`**
+Please read this [community blog post][freeing-maintainers] for details.
-## Submitting a Pull Request
+
+
-Pull requests should be focused on a single exercise, issue, or conceptually cohesive change. Please refer to Exercism's [pull request guidelines](https://github.com/exercism/legacy-docs/blob/main/contributing/pull-request-guidelines.md).
+Here to suggest a new feature or new exercise?? **Hooray!** π
+We'd love if you did that via our [Exercism Community Forum](https://forum.exercism.org/).
+Please read [Suggesting Exercise Improvements][suggesting-improvements] & [Chesterton's Fence][chestertons-fence].
+_Thoughtful suggestions will likely result faster & more enthusiastic responses from volunteers._
-Please follow the coding standards for Rust. [rustfmt](https://github.com/nrc/rustfmt) may help with this
-and can be installed with `cargo install rustfmt`.
+
+
-### Verifying your Change
+β¨ π¦ _**Want to jump directly into Exercism specifications & detail?**_
+ [Structure][exercism-track-structure] **|** [Tasks][exercism-tasks] **|** [Concepts][exercism-concepts] **|** [Concept Exercises][concept-exercises] **|** [Practice Exercises][practice-exercises] **|** [Presentation][exercise-presentation]
+ [Writing Style Guide][exercism-writing-style] **|** [Markdown Specification][exercism-markdown-specification] (_β¨ version in [contributing][website-contributing-section] on exercism.org_)
-Before submitting your pull request, you'll want to verify the changes in two ways:
+
+
-* Run all the tests for the Rust exercises
-* Run an Exercism-specific linter to verify the track
+## Exercism Rust Track License
-All the tests for Rust exercises can be run from the top level of the repo with `_test/check_exercises.sh`. If you are on a Windows machine, there are additional [Windows-specific instructions](_test/WINDOWS_README.md) for running this.
+This repository uses the [MIT License](/LICENSE).
-### On modifying the exercises' README
-
-Please note that the README of every exercise is formed using several templates, not all of which are necessarily present on this repo. The most important of these:
-
-- The `description.md` file in the exercise directory from the [problem-specifications repository](https://github.com/exercism/problem-specifications/tree/main/exercises)
-
-- The `.meta/hints.md` file in the exercise directory on this repository
-
-- The [Rust-specific instructions](https://github.com/exercism/rust/blob/main/exercises/shared/.docs/tests.md)
-
-If you are modifying the section of the README that belongs to the template not from this repository, please consider [opening a PR](https://github.com/exercism/problem-specifications/pulls) on the `problem-specifications` repository first.
-
-## Contributing a New Exercise
-
-Please see the documentation about [adding new exercises](https://github.com/exercism/legacy-docs/blob/main/you-can-help/make-up-new-exercises.md).
-
-Note that:
-
-- The simplest way to generate, update or configure an exercise is to use the [exercise](https://github.com/exercism/rust/tree/main/util/exercise) utility provided in this repository. To compile the utility you can use the [bin/build_exercise_crate.sh](https://github.com/exercism/rust/tree/main/bin/build_exercise_crate.sh) script or, if the script does not work for you, use the `cargo build --release` command in the `util/exercise/` directory and then copy the `exercise` binary from the `util/exercise/target/release/` directory into the `bin/` directory. Use `bin/exercise --help` to learn about the existing commands and their possible usage.
-
-- Each exercise must stand on its own. Do not reference files outside the exercise directory. They will not be included when the user fetches the exercise.
-
-- Exercises must conform to the Exercism-wide standards described in [the documentation](https://github.com/exercism/legacy-docs/tree/main/language-tracks/exercises).
-
-- Each exercise should have:
-
- exercises/exercise-name/
- tests/exercise-name.rs <- a test suite
- src/lib.rs <- an empty file or with exercise stubs
- example.rs <- example solution that satisfies tests
- Cargo.toml <- with version equal to exercise definition
- Cargo.lock <- Auto generated
- README.md <- Instructions for the exercise (see notes below)
-
-- The stub file and test suite should use only the Rust core libraries. `Cargo.toml` should not list any external dependencies as we don't want to make the student assume required crates. If an `example.rs` uses external crates, include `Cargo-example.toml` so that `_tests/check_exercises.sh` can compile with these when testing.
-
-- Except in extraordinary circumstances, the stub file should compile under `cargo test --no-run`.
- This allows us to check that the signatures in the stub file match the signatures expected by the tests.
- Use `unimplemented!()` as the body of each function to achieve this.
- If there is a justified reason why this is not possible, instead include a `.custom."allowed-to-not-compile"` key in the exercise's `.meta/config.json` containing the reason.
-
-- If porting an existing exercise from problem-specifications that has a `canonical-data.json` file, use the version in `canonical-data.json` for that exercise as your `Cargo.toml` version. Otherwise, use "0.0.0".
-
-- An exercise may contain `.meta/hints.md`. This is optional and will appear after the normal exercise
- instructions if present. Rust is different in many ways from other languages. This is a place where the differences required for Rust are explained. If it is a large change, you may want to call this out as a comment at the top of `src/lib.rs`, so the user recognizes to read this section before starting.
-
-- If the test suite is appreciably sped up by running in release mode, and there is reason to be confident that the test suite appropriately detects any overflow errors, consider adding a marker to the exercise's `.meta/config.json`: `.custom."test-in-release-mode"` should be `true`. This can particularly impact the online editor experience.
-
-- If your exercise implements macro-based testing (see [#392](https://github.com/exercism/rust/issues/392#issuecomment-343865993) and [`perfect-numbers.rs`](https://github.com/exercism/rust/blob/main/exercises/practice/perfect-numbers/tests/perfect-numbers.rs)), you will likely run afoul of a CI check which counts the `#[ignore]` lines and compares the result to the number of `#[test]` lines. To fix this, add a marker to the exercise's `.meta/config.json`: `.custom."ignore-count-ignores"` should be `true` to disable that check for your exercise.
-
-- `README.md` may be [regenerated](https://github.com/exercism/legacy-docs/blob/main/maintaining-a-track/regenerating-exercise-readmes.md) from Exercism data. The generator will use the `description.md` from the exercise directory in the [problem-specifications repository](https://github.com/exercism/problem-specifications/tree/main/exercises), then any hints in `.meta/hints.md`, then the [Rust-specific instructions](https://github.com/exercism/rust/blob/main/exercises/shared/.docs/tests.md). The `## Source` section comes from the `metadata.yml` in the same directory. Convention is that the description of the source remains text and the link is both name and hyperlink of the markdown link.
-
-- Be sure to add the exercise to an appropriate place in the `config.json` file. The position in the file determines the order exercises are sent. Generate a unique UUID for the exercise. Current difficulty levels in use are 1, 4, 7 and 10.
+[being-a-good-community-member]: https://github.com/exercism/docs/tree/main/community/good-member
+[chestertons-fence]: https://github.com/exercism/docs/blob/main/community/good-member/chestertons-fence.md
+[concept-exercises]: https://github.com/exercism/docs/blob/main/building/tracks/concept-exercises.md
+[exercise-presentation]: https://github.com/exercism/docs/blob/main/building/tracks/presentation.md
+[exercism-admins]: https://github.com/exercism/docs/blob/main/community/administrators.md
+[exercism-code-of-conduct]: https://exercism.org/docs/using/legal/code-of-conduct
+[exercism-concepts]: https://github.com/exercism/docs/blob/main/building/tracks/concepts.md
+[exercism-contributors]: https://github.com/exercism/docs/blob/main/community/contributors.md
+[exercism-markdown-specification]: https://github.com/exercism/docs/blob/main/building/markdown/markdown.md
+[exercism-mentors]: https://github.com/exercism/docs/tree/main/mentoring
+[exercism-tasks]: https://exercism.org/docs/building/product/tasks
+[exercism-track-maintainers]: https://github.com/exercism/docs/blob/main/community/maintainers.md
+[exercism-track-structure]: https://github.com/exercism/docs/tree/main/building/tracks
+[exercism-website]: https://exercism.org/
+[exercism-writing-style]: https://github.com/exercism/docs/blob/main/building/markdown/style-guide.md
+[freeing-maintainers]: https://exercism.org/blog/freeing-our-maintainers
+[practice-exercises]: https://github.com/exercism/docs/blob/main/building/tracks/practice-exercises.md
+[suggesting-improvements]: https://github.com/exercism/docs/blob/main/community/good-member/suggesting-exercise-improvements.md
+[the-words-that-we-use]: https://github.com/exercism/docs/blob/main/community/good-member/words.md
+[website-contributing-section]: https://exercism.org/docs/building
+[the-rust-programming-language]: https://doc.rust-lang.org/book/
diff --git a/_test/WINDOWS_README.md b/_test/WINDOWS_README.md
deleted file mode 100644
index 79555a8b5..000000000
--- a/_test/WINDOWS_README.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# check_exercises.sh for Windows Rust Developers
-
-It is possible to run `check_exercises.sh` on Windows 10, pointing to the Windows location for your GitHub repository. This is done with the Ubuntu on Windows subsystem.
-
-## Enable Developer Mode
-To run Ubuntu on Windows, you need to be in Developer Mode.
-
- - Open Settings
- - Open Update and Security
- - Select For Developers on Left Side
- - Change to Developer Mode from Sideload Apps
-
-## Install
-
-Start a PowerShell as Administrator.
-
-Run the following:
-
- Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux
-
-## Run bash
-
-The `bash` command now gives you a terminal in a Ubuntu Linux instance. You have access to Windows files via /mnt/[drive_letter]
-
-Example: Windows user directory would be
-
- /mnt/c/Users/username
-
-## Installing Rust
-
-Inside bash, you will not have access to Window's Rust. You need to install the Linux version of Rust.
-
- curl -sf -L https://static.rust-lang.org/rustup.sh | sh
-
-You also need to install a cc linker for Rust.
-
- sudo apt-get install build-essential
-
-## Running Tests
-
- cd /mnt/c/[path of github project]
- _test/check_exercises.sh
-
-This will re-download and build any crates needed, as they only existed in your Windows Rust.
diff --git a/_test/check_exercises_for_authors.sh b/_test/check_exercises_for_authors.sh
deleted file mode 100755
index d3c3fb10a..000000000
--- a/_test/check_exercises_for_authors.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env bash
-
-repo=$(cd "$(dirname "$0")/.." && pwd)
-
-if grep -rnw "$repo/exercises/" --include="*.toml" -e "authors"; then
- echo "Found 'authors' field in exercises";
- exit 1;
-fi
diff --git a/_test/check_uuids.sh b/_test/check_uuids.sh
deleted file mode 100755
index ae2ae60c1..000000000
--- a/_test/check_uuids.sh
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/env bash
-set -eo pipefail
-
-repo=$(cd "$(dirname "$0")/.." && pwd)
-
-# Check for invalid UUIDs.
-# can be removed once `configlet lint` gains this ability.
-# Check issue https://github.com/exercism/configlet/issues/99
-
-bad_uuid=$(jq --raw-output '.exercises | .concept[], .practice[] | .uuid' "$repo"/config.json | grep -vE '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' || test 1)
-if [ -n "$bad_uuid" ]; then
- echo "invalid UUIDs found! please correct these to be valid UUIDs:"
- echo "$bad_uuid"
- exit 1
-fi
diff --git a/_test/count_ignores.sh b/_test/count_ignores.sh
deleted file mode 100755
index 22bbba105..000000000
--- a/_test/count_ignores.sh
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/usr/bin/env bash
-
-repo=$(cd "$(dirname "$0")/.." && pwd)
-exitcode=0
-
-for e in "$repo"/exercises/*/*; do
- # An exercise must have a .meta/config.json
- metaconf="$e/.meta/config.json"
- if [ ! -f "$metaconf" ]; then
- continue
- fi
-
- if jq --exit-status '.custom?."ignore-count-ignores"?' "$metaconf"; then
- continue
- fi
- if [ -d "$e/tests" ]; then
- total_tests=0
- total_ignores=0
- for t in "$e"/tests/*.rs; do
- tests=$(grep -c "\#\[test\]" "$t" | tr -d '[:space:]')
- ignores=$(grep -c "\#\[ignore\]" "$t" | tr -d '[:space:]')
-
- total_tests=$((total_tests + tests))
- total_ignores=$((total_ignores + ignores))
- done
- want_ignores=$((total_tests - 1))
- if [ "$total_ignores" != "$want_ignores" ]; then
- # ShellCheck wants us to use printf,
- # but there are no other uses of printf in this repo,
- # so printf hasn't been tested to work yet.
- # (We would not be opposed to using printf and removing this disable;
- # we just haven't tested it to confirm it works yet).
- # shellcheck disable=SC2028
- echo "\033[1;31m$e: Has $total_tests tests and $total_ignores ignores (should be $want_ignores)\033[0m"
- exitcode=1
- fi
- fi
-done
-
-exit $exitcode
diff --git a/_test/ensure_lib_src_rs_exist.sh b/_test/ensure_lib_src_rs_exist.sh
deleted file mode 100755
index 0c0205577..000000000
--- a/_test/ensure_lib_src_rs_exist.sh
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/usr/bin/env bash
-
-repo=$(cd "$(dirname "$0")/.." && pwd)
-
-missing=""
-
-empty_stub=""
-
-check_status=0
-
-IGNORED_EXERCISES=(
- "two-fer" #deprecated
- "nucleotide-codons" #deprecated
- "hexadecimal" #deprecated
-)
-
-for dir in "$repo"/exercises/*/*/; do
- exercise=$(basename "$dir")
-
- if [ ! -f "$dir/src/lib.rs" ]; then
- echo "$exercise is missing a src/lib.rs stub file. Please create the missing file with the template, that is necessary for the exercise, present in it."
- missing="$missing\n$exercise"
- else
- #Check if the stub file is empty
- if [ ! -s "$dir/src/lib.rs" ] || [ "$(cat "$dir/src/lib.rs")" == "" ] && [[ " ${IGNORED_EXERCISES[*]} " != *"$exercise"* ]]; then
- echo "$exercise has src/lib.rs stub file, but it is empty."
- empty_stub="$empty_stub\n$exercise"
- fi
- fi
-done
-
-if [ -n "$missing" ]; then
- # extra echo to generate a new line
- echo
- echo "Exercises missing src/lib.rs:$missing"
-
- check_status=1
-fi
-
-if [ -n "$empty_stub" ]; then
- echo
- echo "Exercises with empty src/lib.rs stub file:$empty_stub"
-
- check_status=1
-fi
-
-if [ "$check_status" -ne 0 ]; then
- exit 1
-else
- exit 0
-fi
diff --git a/_test/verify_exercise_difficulties.sh b/_test/verify_exercise_difficulties.sh
deleted file mode 100755
index e7bb6179f..000000000
--- a/_test/verify_exercise_difficulties.sh
+++ /dev/null
@@ -1,56 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-repo=$(cd "$(dirname "$0")/.." && pwd)
-config=$repo/config.json
-
-es=0
-
-# ensure every exercise has a difficulty
-no_difficulty=$(
- jq --raw-output '
- .exercises |
- .concept[], .practice[] |
- select((.status != "deprecated") and (has("difficulty") | not)) |
- .slug
- ' "$config"
-)
-if [ -n "$no_difficulty" ]; then
- echo "Exercises without a difficulty in config.json:"
- echo "$no_difficulty"
- es=1
-fi
-
-# ensure that all difficulties are one of 1, 4, 7, 10
-invalid_difficulty=$(
- jq --raw-output '
- .exercises |
- .concept[], .practice[] |
- select(
- (.status != "deprecated") and
- has("difficulty") and
- (
- .difficulty | tostring |
- in({"1":null,"4":null,"7":null,"10":null}) |
- not
- )
- ) |
- "\(.slug) (\(.difficulty))"
- ' "$config"
-)
-if [ -n "$invalid_difficulty" ]; then
- echo "Exercises with invalid difficulty (must be in {1, 4, 7, 10})"
- echo "$invalid_difficulty"
- es=1
-fi
-
-# ensure difficulties are sorted
-#exercise_order=$(jq --raw-output '.exercises[] | select(.deprecated | not) | .slug' $config)
-#sorted_order=$(jq --raw-output '.exercises | sort_by(.difficulty) | .[] | select(.deprecated | not) | .slug' $config)
-#if [ "$exercise_order" != "$sorted_order" ]; then
-# echo "Exercises are not in sorted order in config.json"
-# es=1
-#fi
-
-exit $es
diff --git a/bin/.shellcheckrc b/bin/.shellcheckrc
deleted file mode 100644
index f229171c6..000000000
--- a/bin/.shellcheckrc
+++ /dev/null
@@ -1,3 +0,0 @@
-shell=bash
-external-sources=true
-disable=SC2001
diff --git a/bin/add_practice_exercise b/bin/add_practice_exercise
deleted file mode 100755
index a9068e431..000000000
--- a/bin/add_practice_exercise
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/usr/bin/env bash
-
-# see comment in generator-utils/utils.sh
-# shellcheck source=bin/generator-utils/utils.sh
-# shellcheck source=bin/generator-utils/templates.sh
-# shellcheck source=bin/generator-utils/prompts.sh
-# shellcheck source=./generator-utils/utils.sh
-# shellcheck source=./generator-utils/prompts.sh
-# shellcheck source=./generator-utils/templates.sh
-
-source ./bin/generator-utils/utils.sh
-source ./bin/generator-utils/prompts.sh
-source ./bin/generator-utils/templates.sh
-
-# Exit if anything fails.
-set -euo pipefail
-
-# If argument not provided, print usage and exit
-if [ $# -ne 1 ] && [ $# -ne 2 ] && [ $# -ne 3 ]; then
- echo "Usage: bin/add_practice_exercise [difficulty] [author-github-handle]"
- exit 1
-fi
-
-# Check if sed is gnu-sed
-if ! sed --version | grep -q "GNU sed"; then
- echo "GNU sed is required. Please install it and make sure it's in your PATH."
- exit 1
-fi
-
-# Check if jq and curl are installed
-command -v jq >/dev/null 2>&1 || {
- echo >&2 "jq is required but not installed. Please install it and make sure it's in your PATH."
- exit 1
-}
-command -v curl >/dev/null 2>&1 || {
- echo >&2 "curl is required but not installed. Please install it and make sure it's in your PATH."
- exit 1
-}
-
-# Build configlet
-bin/fetch-configlet
-message "success" "Fetched configlet successfully!"
-
-# Check if exercise exists in configlet info or in config.json
-check_exercise_existence "$1"
-
-# ==================================================
-
-slug="$1"
-# Fetch canonical data
-canonical_json=$(bin/fetch_canonical_data "$slug")
-
-has_canonical_data=true
-if [ "${canonical_json}" == "404: Not Found" ]; then
- has_canonical_data=false
- message "warning" "This exercise doesn't have canonical data"
-
-else
- echo "$canonical_json" >canonical_data.json
- message "success" "Fetched canonical data successfully!"
-fi
-
-underscored_slug=$(dash_to_underscore "$slug")
-exercise_dir="exercises/practice/${slug}"
-exercise_name=$(format_exercise_name "$slug")
-message "info" "Using ${yellow}${exercise_name}${blue} as a default exercise name. You can edit this later in the config.json file"
-# using default value for difficulty
-exercise_difficulty=$(validate_difficulty_input "${2:-$(get_exercise_difficulty)}")
-message "info" "The exercise difficulty has been set to ${yellow}${exercise_difficulty}${blue}. You can edit this later in the config.json file"
-# using default value for author
-author_handle=${3:-$(get_author_handle)}
-message "info" "Using ${yellow}${author_handle}${blue} as author's handle. You can edit this later in the 'authors' field in the ${exercise_dir}/.meta/config.json file"
-
-create_rust_files "$exercise_dir" "$slug" "$has_canonical_data"
-
-# ==================================================
-
-
-# Preparing config.json
-message "info" "Adding instructions and configuration files..."
-
-uuid=$(bin/configlet uuid)
-
-# Add exercise-data to global config.json
-jq --arg slug "$slug" --arg uuid "$uuid" --arg name "$exercise_name" --argjson difficulty "$exercise_difficulty" \
- '.exercises.practice += [{slug: $slug, name: $name, uuid: $uuid, practices: [], prerequisites: [], difficulty: $difficulty}]' \
- config.json >config.json.tmp
-# jq always rounds whole numbers, but average_run_time needs to be a float
-sed -i 's/"average_run_time": \([0-9]\+\)$/"average_run_time": \1.0/' config.json.tmp
-mv config.json.tmp config.json
-message "success" "Added instructions and configuration files"
-
-# Create instructions and config files
-echo "Creating instructions and config files"
-
-./bin/configlet sync --update --yes --docs --metadata --exercise "$slug"
-./bin/configlet sync --update --yes --filepaths --exercise "$slug"
-./bin/configlet sync --update --tests include --exercise "$slug"
-message "success" "Created instructions and config files"
-
-# Push author to "authors" array in ./meta/config.json
-meta_config="$exercise_dir"/.meta/config.json
-jq --arg author "$author_handle" '.authors += [$author]' "$meta_config" >"$meta_config".tmp && mv "$meta_config".tmp "$meta_config"
-message "success" "You've been added as the author of this exercise."
-
-sed -i "s/name = \".*\"/name = \"$underscored_slug\"/" "$exercise_dir"/Cargo.toml
-
-message "done" "All stub files were created."
-
-message "info" "After implementing the solution, tests and configuration, please run:"
-
-echo "./bin/configlet fmt --update --yes --exercise ${slug}"
diff --git a/_test/check_exercises.sh b/bin/check_exercises.sh
similarity index 79%
rename from _test/check_exercises.sh
rename to bin/check_exercises.sh
index 5ec830c38..0b69cfea7 100755
--- a/_test/check_exercises.sh
+++ b/bin/check_exercises.sh
@@ -1,16 +1,5 @@
#!/usr/bin/env bash
-# test for existence and executability of the test-exercise script
-# this depends on that
-if [ ! -f "./bin/test-exercise" ]; then
- echo "bin/test-exercise does not exist"
- exit 1
-fi
-if [ ! -x "./bin/test-exercise" ]; then
- echo "bin/test-exercise does not have its executable bit set"
- exit 1
-fi
-
# In DENYWARNINGS or CLIPPY mode, do not set -e so that we run all tests.
# This allows us to see all warnings.
# If we are in neither DENYWARNINGS nor CLIPPY mode, do set -e.
@@ -19,14 +8,14 @@ if [ -z "$DENYWARNINGS" ] && [ -z "$CLIPPY" ]; then
fi
# can't benchmark with a stable compiler; to bench, use
-# $ BENCHMARK=1 rustup run nightly _test/check_exercises.sh
+# $ BENCHMARK=1 rustup run nightly bin/check_exercises.sh
if [ -n "$BENCHMARK" ]; then
target_dir=benches
else
target_dir=tests
fi
-repo=$(cd "$(dirname "$0")/.." && pwd)
+repo=$(git rev-parse --show-toplevel)
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
files="$(
@@ -70,11 +59,11 @@ for exercise in $files; do
# (such as "Compiling"/"Downloading").
# Compiler errors will still be shown though.
# Both flags are necessary to keep things quiet.
- ./bin/test-exercise "$directory" --quiet --no-run
+ ./bin/test_exercise.sh "$directory" --quiet --no-run
return_code=$((return_code | $?))
else
# Run the test and get the status
- ./bin/test-exercise "$directory" $release
+ ./bin/test_exercise.sh "$directory" $release
return_code=$((return_code | $?))
fi
done
diff --git a/bin/clean_topics_vs_practices.py b/bin/clean_topics_vs_practices.py
deleted file mode 100755
index ed3b55d01..000000000
--- a/bin/clean_topics_vs_practices.py
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/env python3
-import json
-
-
-def main():
- with open("config.json", encoding="utf-8") as f:
- config = json.load(f)
-
- concepts = {c['slug'] for c in config['concepts']}
-
- for practice_exercise in config['exercises']['practice']:
- if practice_exercise['topics'] is None:
- continue
-
- practice_exercise['practices'].extend((topic for topic in practice_exercise['topics'] if topic in concepts))
- practice_exercise['topics'] = [topic for topic in practice_exercise['topics'] if topic not in concepts]
-
- for concept in concepts:
- count = 0
- for practice_exercise in config['exercises']['practice']:
- if concept in practice_exercise['practices']:
- count += 1
- if count > 10:
- practice_exercise['practices'].remove(concept)
- practice_exercise['topics'].append(concept)
-
- for practice_exercise in config['exercises']['practice']:
- practice_exercise['practices'].sort()
-
- if practice_exercise['topics'] is not None:
- practice_exercise['topics'].sort()
-
-
- with open("config.json", 'w', encoding="utf-8") as f:
- json.dump(config, f, indent=2, ensure_ascii=False)
- f.write('\n')
-
- print("Updated config.json")
-
-
-if __name__ == '__main__':
- main()
diff --git a/_test/ensure_stubs_compile.sh b/bin/ensure_stubs_compile.sh
similarity index 98%
rename from _test/ensure_stubs_compile.sh
rename to bin/ensure_stubs_compile.sh
index 6f154ae31..623c2d1cd 100755
--- a/_test/ensure_stubs_compile.sh
+++ b/bin/ensure_stubs_compile.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
-repo="$(cd "$(dirname "$0")/.." && pwd)"
+repo="$(git rev-parse --show-toplevel)"
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
changed_exercises="$(
diff --git a/bin/fetch_canonical_data b/bin/fetch_canonical_data
deleted file mode 100755
index 4226a4b92..000000000
--- a/bin/fetch_canonical_data
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-# This script fetches the canonical data of the exercise.
-
-
-# Exit if anything fails.
-set -euo pipefail
-
-
-if [ $# -ne 1 ]; then
- echo "Usage: bin/fetch_canonical_data "
- exit 1
-fi
-
-# check if curl is installed
-command -v curl >/dev/null 2>&1 || {
- echo >&2 "curl is required but not installed. Please install it and make sure it's in your PATH."
- exit 1
-}
-
-slug=$1
-
-curlopts=(
- --silent
- --retry 3
- --max-time 4
-)
-curl "${curlopts[@]}" "https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/${slug}/canonical-data.json"
diff --git a/bin/format_exercises b/bin/format_exercises.sh
similarity index 86%
rename from bin/format_exercises
rename to bin/format_exercises.sh
index 7798baea7..8d8fa2d71 100755
--- a/bin/format_exercises
+++ b/bin/format_exercises.sh
@@ -2,14 +2,14 @@
# Format existing exercises using rustfmt
set -e
-RUST_TRACK_REPO_PATH=$(cd "$(dirname "$0")/.." && pwd)
+repo=$(git rev-parse --show-toplevel)
# traverse either concept or practice exercise
# directory and format Rust files
format_exercises() {
- EXERCISES_PATH="${RUST_TRACK_REPO_PATH}/exercises/$1"
+ exercises_path="$repo/exercises/$1"
source_file_name="$2"
- for exercise_dir in "${EXERCISES_PATH}"/*; do
+ for exercise_dir in "$exercises_path"/*; do
(
cd "$exercise_dir"
config_file="$exercise_dir/.meta/config.json"
diff --git a/bin/generate_tests b/bin/generate_tests
deleted file mode 100755
index 1065148a2..000000000
--- a/bin/generate_tests
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env bash
-
-# Exit if anything fails.
-set -euo pipefail
-
-# see comment in generator-utils/utils.sh
-# shellcheck source=bin/generator-utils/utils.sh
-# shellcheck source=./generator-utils/utils.sh
-source ./bin/generator-utils/utils.sh
-
-if [ ! -e bin/generator-utils/escape_double_quotes ]; then
- message "info" "Building util function"
- cd util/escape_double_quotes && ./build && cd ../..
-fi
-
-digest_template() {
- local template
- template=$(bin/generator-utils/escape_double_quotes bin/test_template)
- # Turn every token into a jq command
-
- echo "$template" | sed 's/${\([^}]*\)\}\$/$(echo $case | jq -r '\''.\1'\'')/g'
-}
-
-message "info" "Generating tests.."
-canonical_json=$(cat canonical_data.json)
-
-slug=$(echo "$canonical_json" | jq '.exercise')
-# Remove double quotes
-slug=$(echo "$slug" | sed 's/"//g')
-exercise_dir="exercises/practice/$slug"
-test_file="$exercise_dir/tests/$slug.rs"
-
-cat <"$test_file"
-use $(dash_to_underscore "$slug")::*;
-
-EOT
-
-# Flattens canonical json, extracts only the objects with a uuid
-cases=$(echo "$canonical_json" | jq '[ .. | objects | with_entries(select(.key | IN("uuid", "description", "input", "expected", "property"))) | select(. != {}) | select(has("uuid")) ]')
-
-# Shellcheck doesn't recognize that `case` is not unused
-
-# shellcheck disable=SC2034
-jq -c '.[]' <<<"$cases" | while read -r case; do
-
- # Evaluate the bash parts and replace them with their return values
- eval_template="$(digest_template | sed -e "s/\$(\(.*\))/\$\(\1\)/g")"
- eval_template="$(eval "echo \"$eval_template\"")"
-
- # Turn function name into snake_case
- formatted_template=$(echo "$eval_template" | sed -E -e '/^fn/!b' -e 's/[^a-zA-Z0-9_{}()[:space:]-]//g' -e 's/([[:upper:]])/ \L\1/g' -e 's/(fn[[:space:]]+)([a-z0-9_-]+)/\1\L\2/g' -e 's/ /_/g' -e 's/_\{/\{/g' -e 's/-/_/g' | sed 's/fn_/fn /' | sed 's/__\+/_/g')
-
- # Push to test file
- echo "$formatted_template" >>"$test_file"
- printf "\\n" >>"$test_file"
-
-done
-
-rustfmt "$test_file"
-
-message "success" "Generated tests successfully! Check out ${test_file}"
diff --git a/bin/generator-utils/colors.sh b/bin/generator-utils/colors.sh
deleted file mode 100644
index d3c5f9339..000000000
--- a/bin/generator-utils/colors.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/usr/bin/env bash
-
-reset_color=$(echo -e '\033[0m')
-
-red=$(echo -e '\033[0;31m')
-green=$(echo -e '\033[0;32m')
-yellow=$(echo -e '\033[0;33m')
-blue=$(echo -e '\033[0;34m')
-cyan=$(echo -e '\033[0;36m')
-
-bold_green=$(echo -e '\033[1;32m')
-
-export red green blue yellow bold_green reset_color cyan
diff --git a/bin/generator-utils/prompts.sh b/bin/generator-utils/prompts.sh
deleted file mode 100644
index fdec283f8..000000000
--- a/bin/generator-utils/prompts.sh
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/usr/bin/env bash
-
-# see comment in utils.sh
-# shellcheck source=bin/generator-utils/colors.sh
-# shellcheck source=./colors.sh
-source ./bin/generator-utils/colors.sh
-
-get_exercise_difficulty() {
- read -rp "Difficulty of exercise (1-10): " exercise_difficulty
- echo "$exercise_difficulty"
-}
-
-validate_difficulty_input() {
- local valid_input=false
- while ! $valid_input; do
- if [[ "$1" =~ ^[1-9]$|^10$ ]]; then
- local exercise_difficulty=$1
- local valid_input=true
- else
- read -rp "${red}Invalid input. ${reset_color}Please enter an integer between 1 and 10. " exercise_difficulty
-
- [[ "$exercise_difficulty" =~ ^[1-9]$|^10$ ]] && valid_input=true
-
- fi
- done
- echo "$exercise_difficulty"
-}
-
-get_author_handle() {
- local default_author_handle
- default_author_handle="$(git config user.name)"
-
- if [ -z "$default_author_handle" ]; then
- read -rp "Hey! Couldn't find your Github handle. Add it now or skip with enter and add it later in the .meta.config.json file: " author_handle
- else
- local author_handle="$default_author_handle"
-
- fi
- echo "$author_handle"
-}
diff --git a/bin/generator-utils/templates.sh b/bin/generator-utils/templates.sh
deleted file mode 100755
index 8be909592..000000000
--- a/bin/generator-utils/templates.sh
+++ /dev/null
@@ -1,157 +0,0 @@
-#!/usr/bin/env bash
-
-# see comment in utils.sh
-# shellcheck source=bin/generator-utils/utils.sh
-# shellcheck source=./utils.sh
-source ./bin/generator-utils/utils.sh
-
-create_fn_name() {
- local slug=$1
- local has_canonical_data=$2
-
- if [ "$has_canonical_data" == false ]; then
- fn_name=$(dash_to_underscore "$slug")
- local fn_name
-
- else
- fn_name=$(jq -r 'first(.. | .property? // empty)' canonical_data.json)
- fi
-
- echo "$fn_name"
-
-}
-
-create_test_file_template() {
- local exercise_dir=$1
- local slug=$2
- local has_canonical_data=$3
- local test_file="${exercise_dir}/tests/${slug}.rs"
-
- cat <"$test_file"
-use $(dash_to_underscore "$slug")::*;
-// Add tests here
-
-EOT
-
- if [ "$has_canonical_data" == false ]; then
-
- cat <>"$test_file"
-// As there isn't a canonical data file for this exercise, you will need to craft your own tests.
-// If you happen to devise some outstanding tests, do contemplate sharing them with the community by contributing to this repository:
-// https://github.com/exercism/problem-specifications/tree/main/exercises/${slug}
-EOT
- message "info" "This exercise doesn't have canonical data."
- message "success" "Stub file for tests has been created!"
- else
-
- local canonical_json
-
- canonical_json=$(cat canonical_data.json)
-
- # sometimes canonical data has multiple levels with multiple `cases` arrays.
- #(see kindergarten-garden https://github.com/exercism/problem-specifications/blob/main/exercises/kindergarten-garden/canonical-data.json)
- # so let's flatten it
-
- local cases
- cases=$(echo "$canonical_json" | jq '[ .. | objects | with_entries(select(.key | IN("uuid", "description", "input", "expected"))) | select(. != {}) | select(has("uuid")) ]')
- local fn_name
-
- fn_name=$(echo "$canonical_json" | jq -r 'first(.. | .property? // empty)')
-
- first_iteration=true
- # loop through each object
- jq -c '.[]' <<<"$cases" | while read -r case; do
- desc=$(echo "$case" | jq '.description' | tr '[:upper:]' '[:lower:]' | tr ' ' '_' | tr -cd '[:alnum:]_' | sed 's/^/test_/')
- input=$(echo "$case" | jq -c '.input')
- expected=$(echo "$case" | jq -c '.expected')
-
- # append each test fn to the test file
- cat <>"$test_file"
-#[test] $([[ "$first_iteration" == false ]] && printf "\n#[ignore]")
-fn ${desc}() {
- let input = ${input};
- let expected = ${expected};
-
- // TODO: Verify assertion
- assert_eq!(${fn_name}(input), expected);
-}
-
-EOT
- first_iteration=false
- done
- message "success" "Stub file for tests has been created and populated with canonical data!"
- fi
-
-}
-
-create_lib_rs_template() {
- local exercise_dir=$1
- local slug=$2
- local has_canonical_data=$3
- local fn_name
-
- fn_name=$(create_fn_name "$slug" "$has_canonical_data")
- cat <"${exercise_dir}/src/lib.rs"
-pub fn ${fn_name}() {
- unimplemented!("implement ${slug} exercise");
-}
-EOT
- message "success" "Stub file for lib.rs has been created!"
-}
-
-overwrite_gitignore() {
-
- local exercise_dir=$1
- cat <"$exercise_dir"/.gitignore
-# Generated by Cargo
-# Will have compiled files and executables
-/target/
-**/*.rs.bk
-
-# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
-# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
-Cargo.lock
-EOT
- message "success" ".gitignore has been overwritten!"
-}
-
-create_example_rs_template() {
-
- local exercise_dir=$1
- local slug=$2
- local has_canonical_data=$3
-
- local fn_name
-
- fn_name=$(create_fn_name "$slug" "$has_canonical_data")
-
- mkdir "${exercise_dir}/.meta"
- cat <"${exercise_dir}/.meta/example.rs"
-pub fn ${fn_name}() {
- // TODO: Create a solution that passes all the tests
- unimplemented!("implement ${slug} exercise");
-}
-
-EOT
- message "success" "Stub file for example.rs has been created!"
-}
-
-create_rust_files() {
-
- local exercise_dir=$1
- local slug=$2
- local has_canonical_data=$3
-
- message "info" "Creating Rust files"
- cargo new --lib "$exercise_dir" -q
- mkdir -p "$exercise_dir"/tests
- touch "${exercise_dir}/tests/${slug}.rs"
-
- create_test_file_template "$exercise_dir" "$slug" "$has_canonical_data"
- create_lib_rs_template "$exercise_dir" "$slug" "$has_canonical_data"
- create_example_rs_template "$exercise_dir" "$slug" "$has_canonical_data"
- overwrite_gitignore "$exercise_dir"
-
- message "success" "Created Rust files succesfully!"
-
-}
diff --git a/bin/generator-utils/utils.sh b/bin/generator-utils/utils.sh
deleted file mode 100755
index 227a9bcd8..000000000
--- a/bin/generator-utils/utils.sh
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/usr/bin/env bash
-
-# top one gets evaluated
-# relative one is needed for .shellcheckrc to test if files exist in local env
-# absolute one is needed for CI to test if files exist
-# before commit swap these accordingly
-# shellcheck source=bin/generator-utils/colors.sh
-# shellcheck source=./colors.sh
-source ./bin/generator-utils/colors.sh
-
-message() {
- local flag=$1
- local message=$2
-
- case "$flag" in
- "success") printf "${green}%s${reset_color}\n" "[success]: $message" ;;
- "task") printf "${cyan}%s${reset_color}\n" "[task]: $message" ;;
- "info") printf "${blue}%s${reset_color}\n" "[info]: $message" ;;
- "warning") printf "${yellow}%s${reset_color}\n" "[warning]: $message" ;;
- "error") printf "${red}%s${reset_color}\n" "[error]: $message" ;;
- "done")
- echo
- # Generate a dashed line that spans the entire width of the screen.
- local cols
- cols=$(tput cols)
- printf "%*s\n" "$cols" "" | tr " " "-"
- echo
- printf "${bold_green}%s${reset_color}\n" "[done]: $message"
- ;;
- *) echo "Invalid flag: $flag" ;;
- esac
-}
-
-dash_to_underscore() {
- echo "$1" | sed 's/-/_/g'
-}
-
-# exercise_name -> Exercise Name
-
-format_exercise_name() {
- echo "$1" | sed 's/-/ /g; s/\b\(.\)/\u\1/g'
-}
-
-check_exercise_existence() {
- message "info" "Looking for exercise.."
- local slug="$1"
-
- # Check if exercise is already in config.json
- if jq '.exercises.practice | map(.slug)' config.json | grep -q "$slug"; then
- echo "${1} has already been implemented."
- exit 1
- fi
-
- # Fetch configlet and crop out exercise list
- local unimplemented_exercises
- unimplemented_exercises=$(bin/configlet info | sed -n '/With canonical data:/,/Track summary:/p' | sed -e '/\(With\(out\)\? canonical data:\|Track summary:\)/d' -e '/^$/d')
- if echo "$unimplemented_exercises" | grep -q "^$slug$"; then
- message "success" "Exercise has been found!"
- else
- message "error" "Exercise doesn't exist!"
- message "info" "These are the unimplemented practice exercises:
-${unimplemented_exercises}"
-
- # Find closest match to typed-in not-found slug
-
- # See util/ngram for source
- # First it builds a binary for the system of the contributor
- if [ -e bin/generator-utils/ngram ]; then
- echo "${yellow}$(bin/generator-utils/ngram "${unimplemented_exercises}" "$slug")${reset_color}"
- else
- message "info" "Building typo-checker binary for $(uname -m) system."
- cd util/ngram && ./build && cd ../.. && echo "${yellow}$(bin/generator-utils/ngram "${unimplemented_exercises}" "$slug")${reset_color}"
- fi
- exit 1
- fi
-}
diff --git a/bin/lint_markdown.sh b/bin/lint_markdown.sh
index 6f3592c7c..52b75c2ab 100755
--- a/bin/lint_markdown.sh
+++ b/bin/lint_markdown.sh
@@ -1,3 +1,7 @@
#!/usr/bin/env bash
-set -e
-npx markdownlint-cli concepts/**/*.md exercises/**/*.md docs/maintaining.md docs/CONTRIBUTING.md
+set -eo pipefail
+
+npx markdownlint-cli \
+ docs/*.md \
+ concepts/**/*.md \
+ exercises/**/*.md
diff --git a/bin/lint_tool_file_names.sh b/bin/lint_tool_file_names.sh
deleted file mode 100755
index 0c0ef67e7..000000000
--- a/bin/lint_tool_file_names.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env bash
-
-# Require that internal shell script file names use snake_case
-set -eo pipefail
-
-# find a list of files whose names do not match our convention
-errant_files=$(find bin/ _test/ -iname '*\.sh' -exec basename {} \; | grep '[^a-z_.]' || test 1)
-
-if [ -n "$errant_files" ]; then
- echo "These file names do not follow our snake case convention:"
- # find them again to print the whole relative path
- while IFS= read -r file_name; do
- find . -name "$file_name"
- done <<< "$errant_files"
- echo "Please correct them!"
- exit 1
-fi
diff --git a/bin/lint_trailing_spaces.sh b/bin/lint_trailing_spaces.sh
deleted file mode 100755
index 16f021a14..000000000
--- a/bin/lint_trailing_spaces.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env bash
-
-# Report any .toml files that have trailing white space
-# so user can fix them
-set -eo pipefail
-
-files=$(find . \( -iname '*.toml' -o -iname '*.sh' -o -iname '*.rs' \) -exec grep -l '[[:space:]]$' {} \;)
-
-if [ -n "$files" ]; then
- echo "These files have trailing whitespace:"
- echo "$files"
- echo "Our conventions disallow this so please remove the trailing whitespace."
- exit 1
-fi
diff --git a/bin/remove_trailing_whitespace.sh b/bin/remove_trailing_whitespace.sh
deleted file mode 100755
index 5a93912f7..000000000
--- a/bin/remove_trailing_whitespace.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env bash
-
-# removes all trailing whitespaces from *.sh and *.py files in this folder
-find . -type f \( -name "*.sh" -o -name "*.py" \) -exec sed -i 's/[[:space:]]\+$//' {} \;
diff --git a/bin/test-exercise b/bin/test_exercise.sh
similarity index 98%
rename from bin/test-exercise
rename to bin/test_exercise.sh
index ee8d3040e..2e1c04b26 100755
--- a/bin/test-exercise
+++ b/bin/test_exercise.sh
@@ -15,7 +15,7 @@ if [ $# -ge 1 ]; then
# so if you are in the exercise directory and want to pass any
# arguments to cargo, you need to include the local path first.
# I.e. to test in release mode:
- # $ test-exercise . --release
+ # $ test_exercise.sh . --release
shift 1
else
exercise='.'
diff --git a/bin/test_template b/bin/test_template
deleted file mode 100644
index 93b513457..000000000
--- a/bin/test_template
+++ /dev/null
@@ -1,6 +0,0 @@
-#[test]
-#[ignore]
-fn ${description}$() {
-let expected = "${expected}$";
- assert_eq!(${property}$(${input}$), expected);
-}
diff --git a/concepts/methods/about.md b/concepts/methods/about.md
index 14eaafdfb..81280e238 100644
--- a/concepts/methods/about.md
+++ b/concepts/methods/about.md
@@ -59,7 +59,7 @@ If we wish to implement an `info` method to display the basic information of the
we could define this method inside an `impl` block for `Wizard`:
```rust
-impl Wizard {
+impl Wizard {
fn info(&self) {
println!(
"A wizard of age {} who studies in House {:?} at Hogwarts",
diff --git a/concepts/references/about.md b/concepts/references/about.md
index 654ff59ad..63c3917ab 100644
--- a/concepts/references/about.md
+++ b/concepts/references/about.md
@@ -65,7 +65,7 @@ fn check_shapes(constant: &[u8], linear: &[u8], superlinear: &[u8]) -> (bool, bo
// understanding the implementations of the following functions is not necessary for this example
// but are provided should you be interested
-fn is_constant(slice: &[u8]) -> bool {
+fn is_constant(slice: &[u8]) -> bool {
slice
.first()
.map(|first| slice.iter().all(|v| v == first))
@@ -146,7 +146,7 @@ pub fn main() {
}
```
-This works, because the compiler knows that the mutable borrows do not overlap
+This works, because the compiler knows that the mutable borrows do not overlap
```rust
fn add_five(counter: &mut i32) {
diff --git a/config.json b/config.json
index dd533207c..66e160a0e 100644
--- a/config.json
+++ b/config.json
@@ -76,9 +76,9 @@
},
{
"slug": "semi-structured-logs",
+ "uuid": "1924b87a-9246-456f-8fc1-111f922a8cf3",
"name": "Semi Structured Logs",
"difficulty": 1,
- "uuid": "1924b87a-9246-456f-8fc1-111f922a8cf3",
"concepts": [
"enums"
],
@@ -89,8 +89,8 @@
},
{
"slug": "resistor-color",
- "name": "Resistor Color",
"uuid": "51c31e6a-b7ec-469d-8a28-dd821fd857d2",
+ "name": "Resistor Color",
"difficulty": 1,
"concepts": [
"external-crates"
@@ -132,9 +132,9 @@
},
{
"slug": "low-power-embedded-game",
+ "uuid": "7f064e9b-f631-48b1-9ed0-a66e8393ceba",
"name": "Low-Power Embedded Game",
"difficulty": 1,
- "uuid": "7f064e9b-f631-48b1-9ed0-a66e8393ceba",
"concepts": [
"tuples",
"destructuring"
@@ -146,9 +146,9 @@
},
{
"slug": "short-fibonacci",
+ "uuid": "c481e318-ddd7-4f8a-91eb-dadb7315e304",
"name": "A Short Fibonacci Sequence",
"difficulty": 1,
- "uuid": "c481e318-ddd7-4f8a-91eb-dadb7315e304",
"concepts": [
"vec-macro"
],
@@ -160,9 +160,9 @@
},
{
"slug": "rpn-calculator",
+ "uuid": "25cc722b-211d-4271-9381-fdfe16b41301",
"name": "RPN Calculator",
"difficulty": 4,
- "uuid": "25cc722b-211d-4271-9381-fdfe16b41301",
"concepts": [
"vec-stack"
],
@@ -176,9 +176,9 @@
},
{
"slug": "csv-builder",
+ "uuid": "10c9f505-9aef-479f-b689-cb7959572482",
"name": "CSV builder",
"difficulty": 1,
- "uuid": "10c9f505-9aef-479f-b689-cb7959572482",
"concepts": [
"string-vs-str"
],
@@ -1482,7 +1482,7 @@
"practices": [],
"prerequisites": [],
"difficulty": 1,
- "topics": null,
+ "topics": [],
"status": "deprecated"
},
{
@@ -1492,7 +1492,7 @@
"practices": [],
"prerequisites": [],
"difficulty": 1,
- "topics": null,
+ "topics": [],
"status": "deprecated"
},
{
@@ -1514,7 +1514,8 @@
"uuid": "cbccd0c5-eb15-4705-9a4c-0209861f078c",
"practices": [],
"prerequisites": [],
- "difficulty": 4
+ "difficulty": 4,
+ "topics": []
},
{
"slug": "kindergarten-garden",
@@ -1522,7 +1523,8 @@
"uuid": "c27e4878-28a4-4637-bde2-2af681a7ff0d",
"practices": [],
"prerequisites": [],
- "difficulty": 1
+ "difficulty": 1,
+ "topics": []
},
{
"slug": "yacht",
@@ -1530,7 +1532,8 @@
"uuid": "1a0e8e34-f578-4a53-91b0-8a1260446553",
"practices": [],
"prerequisites": [],
- "difficulty": 4
+ "difficulty": 4,
+ "topics": []
}
],
"foregone": [
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 94af02384..a0eb8e3a8 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -1,39 +1,99 @@
# Contributing to the Rust Exercism Track
-This track is a work in progress.
-Please take all the value you can from it, but if you notice any way to improve it, we are eager for pull requests.
-Fixing a typo in documentation is every bit as valid a contribution as a massive addition of code.
+Issues and pull requests are currently being auto-closed.
+Please make a post on [the Exercism forum] to propose changes.
+Contributions are very welcome if they are coordinated on the forum.
-> A work of art is never finished, merely abandoned.
->
-> -- Paul ValΓ©ry
+[the Exercism forum]: https://forum.exercism.org/
-Nonetheless, feel free to peruse what we have written thus far!
+## General policies
-*Contributions welcome :)*
+- [Code of Conduct](https://exercism.org/code-of-conduct)
+- [Exercism's PR guidelines](https://exercism.org/docs/community/being-a-good-community-member/pull-requests).
-As one of the many tracks in Exercism, contributions here should observe Exercism standards like the [Code of Conduct](https://exercism.org/code-of-conduct).
-This document introduces the ways you can help and what maintainers expect of contributors.
+## Tooling
-## Ways to Contribute
+Some tooling is present as bash scripts in `bin/`.
+A lot more is present in `rust-tooling/`,
+which should be preferred for anything non-trivial.
-As with many Open Source projects, work abounds.
-Here are a few categories of welcome contribution:
-- improving existing exercises
-- creating new exercises
-- improving internal tooling
-- updating documentation
-- fixing typos, misspellings, and grammatical errors
+There is also a [`justfile`](https://github.com/casey/just)
+with a couple useful commands to interact with the repo.
+Feel free to extend it.
-## Merging Philosophy
+If you want to run CI tests locally, `just test` will get you quite far.
-A [pull request](https://docs.github.com/en/github/getting-started-with-github/github-glossary#pull-request) should address one logical change.
-This could be small or big.
+## Creating a new exercise
-For example, [#1175](https://github.com/exercism/rust/pull/1175) fixed a single typo in a single file after minutes of collaboration.
+Please familiarize yourself with the [Exercism documentation about practice exercises].
-It was a small, short pull request with one logical change.
+[Exercism documentation about practice exercises]: https://exercism.org/docs/building/tracks/practice-exercises
-[#653](https://github.com/exercism/rust/pull/653) introduced the doubly linked list exercise after months of collaboration.
+Run `just add-practice-exercise` and you'll be prompted for the minimal
+information required to generate the exercise stub for you.
+After that, jump in the generated exercise and fill in any todos you find.
+This includes most notably:
+- adding an example solution in `.meta/example.rs`
+- Adjusting `.meta/test_template.tera`
-It was a big, long-running pull request with one logical change.
+The tests are generated using the template engine [Tera].
+The input of the template is the canonical data from [`problem-specifications`].
+if you want to exclude certain tests from being generated,
+you have to set `include = false` in `.meta/tests.toml`.
+
+[Tera]: https://keats.github.io/tera/docs/
+[`problem-specifications`]: https://github.com/exercism/problem-specifications/
+
+Many aspects of a correctly implemented exercises are checked in CI.
+I recommend that instead of spending lots of time studying and writing
+documentation about the process, *just do it*.
+If something breaks, fix it and add a test / automation
+so it won't happen anymore.
+
+If you are creating a practice exercise from scratch,
+one that is not present in `problem-specifications`,
+you have to write your tests manually.
+Tests should be sorted by increasing complexity,
+so students can un-ignore them one by one and solve the exercise with TDD.
+See [the Exercism documentation](https://github.com/exercism/legacy-docs/blob/main/language-tracks/exercises/anatomy/test-suites.md)
+for more thoughts on writing good tests.
+
+Except for extraordinary circumstances,
+the stub file should compile under `cargo test --no-run`.
+This allows us to check that the signatures in the stub file
+match the signatures expected by the tests.
+Use `todo!()` as the body of each function to achieve this.
+If there is a justified reason why this is not possible,
+include a `.custom."allowed-to-not-compile"` key
+in the exercise's `.meta/config.json` containing the reason.
+
+If your exercise implements macro-based testing
+(see [#392](https://github.com/exercism/rust/issues/392#issuecomment-343865993)
+and [`perfect-numbers.rs`](https://github.com/exercism/rust/blob/main/exercises/practice/perfect-numbers/tests/perfect-numbers.rs)),
+you will likely run afoul of a CI check which counts the `#[ignore]` lines
+and compares the result to the number of `#[test]` lines.
+To fix this, add a marker to the exercise's `.meta/config.json`:
+`.custom."ignore-count-ignores"` should be `true`
+to disable that check for your exercise.
+
+## Updating an exercise
+
+Many exercises are derived from [`problem-specifications`].
+This includes their test suite and user-facing documentation.
+Before proposing changes here,
+check if they should be made `problem-specifications` instead.
+
+Run `just update-practice-exercise` to update an exercise.
+This outsources most work to `configlet sync --update`
+and runs the test generator again.
+
+## Syllabus
+
+The syllabus is currently deactivated due to low quality.
+see [this forum post](https://forum.exercism.org/t/feeling-lost-and-frustrated-in-rust/4882)
+for some background on the desicion.
+
+Creating a better syllabus would be very benefitial,
+but it's a lot of work and requires good communication and coordination.
+Make sure to discuss any plans you have on the forum
+with people who have experience building syllabi.
diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md
index 4e16399e4..8c86bce10 100644
--- a/docs/INSTALLATION.md
+++ b/docs/INSTALLATION.md
@@ -1,42 +1,6 @@
# Installation
-Methods for installing Rust change as the language evolves. To get up-to-date installation instructions head to the [Rust install page](https://www.rust-lang.org/tools/install).
+To get the recommended installation instructions for your platform,
+head over to [the official Rust website].
-## Additional utilities
-
-### Rustfmt - Writing well-formatted code
-
-When you are solving the exercise, you are free to choose any coding format you want.
-However when you are writing a real-world application or a library, your source code will
-be read by other people, not just you. To solve a problem when different people choose
-different formats for a single project, the developers set a standard coding format
-for the said project.
-
-In the Rust world there is a tool, that helps developers to bring standard formatting
-to their applications - [rustfmt](https://github.com/rust-lang/rustfmt).
-
-To install `rustfmt` use the following commands:
-
-```bash
-rustup self update
-
-rustup component add rustfmt
-```
-
-### Clippy - Writing effective code
-
-At its core the process of programming consists of two parts: storing and managing
-the resources of your computer. Rust provides a lot of means to accomplish these two
-task. Unfortunately sometimes programmers do not use those means very effectively and
-create programms that work correctly, but require a lot of resources like memory or time.
-
-To catch the most common ineffective usages of the Rust language,
-a tool was created - [clippy](https://github.com/rust-lang/rust-clippy).
-
-To install `clippy` use the following commands:
-
-```bash
-rustup self update
-
-rustup component add clippy
-```
+[the official Rust website]: https://www.rust-lang.org/tools/install
diff --git a/docs/RESOURCES.md b/docs/RESOURCES.md
index cd050b3ec..6d8ee7ae7 100644
--- a/docs/RESOURCES.md
+++ b/docs/RESOURCES.md
@@ -6,4 +6,4 @@
* [Stack Overflow](http://stackoverflow.com/questions/tagged/rust) can be used to search for your problem and see if it has been answered already. You can also ask and answer questions.
* The [Rust User Forum](http://users.rust-lang.org) is for general discussion about Rust.
* [/r/rust](http://www.reddit.com/r/rust/) is the official Rust subreddit.
-* [The Rust Programming Language](https://discord.gg/rust-lang) official server on [Discord](https://discordapp.com/) can be used for quick queries and discussions about the language.
+* [The Rust Programming Language](https://discord.gg/rust-lang) official server on [Discord](https://discordapp.com/) can be used for quick queries and discussions about the language.
diff --git a/docs/maintaining.md b/docs/maintaining.md
deleted file mode 100644
index d794de3e9..000000000
--- a/docs/maintaining.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# Maintaining Notes
-
-This document captures informal policies, tips, and topics useful to maintaining the Rust track.
-
-## Internal Tooling
-
-We have a number of scripts for CI tests.
-They live in `bin/` and `_test/`.
-
-## Internal Tooling Style Guide
-
-This is non-exhaustive.
-
-- Adopt a Unix philosophy for tooling
- - prefer using tools that do one thing well
- - prefer using tools that are ubiquitous: `jq` or `sed` instead of `prettier` or `sd`
- - write scripts to do one thing well
- - prefer GNU versions of `sed` and other utilities
-- Prefer Bash for scripting
- - Strive for compatibility. macOS still distributes Bash v3.x by default, despite v5.x being current; this means that the scripts can't depend on certain features like map.
-- Scripts should use `#!/usr/bin/env bash` as their shebang
- - This increases portability on NixOS and macOS because contributors' preferred bash may not be installed in `/bin/bash`.
-- Prefer snake case for script file names
-
- ```sh
- hello_world.sh
- ```
-
- - This simplifies development when upgrading a script into a proper language. *Rusty tooling anyone?*
-- Script file names should include the `.sh` extension
-- Set the executable bit on scripts that should be called directly.
-- Scripts should set the following options at the top
-
- ```bash
- set -eo pipefail
- ```
-
-## Running CI Locally
-
-You can run CI tools locally.
-Scripts expect GNU versions of tooling, so you may see unexpected results on macOS.
-[Here](https://github.com/exercism/rust/issues/1138) is one example.
-Windows users can also run tooling locally using [WSL](https://docs.microsoft.com/en-us/windows/wsl/).
-We recommend WSL 2 with the distribution of your choice.
-
-## Maintainer Tips and Tricks
-
-Exercism tracks follow a specification that has evolved over time.
-Maintainers often need to make ad-hoc migrations to files in this repository.
-If you find yourself scripting such ad-hoc changes, include the source of your script using markdown codeblocks in a commit message.
-
-See [this commit](https://github.com/exercism/rust/commit/45eb8cc113a733636212394dee946ceff5949cc3) for an example.
diff --git a/exercises/concept/magazine-cutout/.docs/instructions.md b/exercises/concept/magazine-cutout/.docs/instructions.md
index 9d47432d9..8e367d5d0 100644
--- a/exercises/concept/magazine-cutout/.docs/instructions.md
+++ b/exercises/concept/magazine-cutout/.docs/instructions.md
@@ -27,7 +27,7 @@ assert!(!can_construct_note(&magazine, ¬e));
The function returns `false` since the magazine only contains one instance of `"two"` when the note requires two of them.
-The following input will succeed:
+The following input will succeed:
```rust
let magazine = "Astronomer Amy Mainzer spent hours chatting with Leonardo DiCaprio for Netflix's 'Don't Look Up'".split_whitespace().collect::>();
diff --git a/exercises/concept/role-playing-game/.docs/introduction.md b/exercises/concept/role-playing-game/.docs/introduction.md
index 3ab3b1cf4..53b3e9f6a 100644
--- a/exercises/concept/role-playing-game/.docs/introduction.md
+++ b/exercises/concept/role-playing-game/.docs/introduction.md
@@ -2,10 +2,10 @@
## Null-References
-If you have ever used another programming language (C/C++, Python, Java, Ruby, Lisp, etc.), it is likely that you have encountered `null` or `nil` before.
-The use of `null` or `nil` is the way that these languages indicate that a particular variable has no value.
-However, this makes accidentally using a variable that points to `null` an easy (and frequent) mistake to make.
-As you might imagine, trying to call a function that isn't there, or access a value that doesn't exist can lead to all sorts of bugs and crashes.
+If you have ever used another programming language (C/C++, Python, Java, Ruby, Lisp, etc.), it is likely that you have encountered `null` or `nil` before.
+The use of `null` or `nil` is the way that these languages indicate that a particular variable has no value.
+However, this makes accidentally using a variable that points to `null` an easy (and frequent) mistake to make.
+As you might imagine, trying to call a function that isn't there, or access a value that doesn't exist can lead to all sorts of bugs and crashes.
The creator of `null` went so far as to call it his ['billion-dollar mistake'.](https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/)
## The `Option` Type
diff --git a/exercises/concept/short-fibonacci/.docs/instructions.md b/exercises/concept/short-fibonacci/.docs/instructions.md
index 926d5e848..4b3d57af5 100644
--- a/exercises/concept/short-fibonacci/.docs/instructions.md
+++ b/exercises/concept/short-fibonacci/.docs/instructions.md
@@ -7,6 +7,7 @@ The Fibonacci sequence is a set of numbers where the next element is the sum of
## 1. Create a buffer of `count` zeroes.
Create a function that creates a buffer of `count` zeroes.
+
```rust
let my_buffer = create_buffer(5);
// [0, 0, 0, 0, 0]
@@ -14,8 +15,9 @@ let my_buffer = create_buffer(5);
## 2. List the first five elements of the Fibonacci sequence
-Create a function that returns the first five numbers of the Fibonacci sequence.
+Create a function that returns the first five numbers of the Fibonacci sequence.
Its first five elements are `1, 1, 2, 3, 5`
+
```rust
let first_five = fibonacci();
// [1, 1, 2, 3, 5]
diff --git a/exercises/practice/acronym/.docs/instructions.md b/exercises/practice/acronym/.docs/instructions.md
index e0515b4d1..c62fc3e85 100644
--- a/exercises/practice/acronym/.docs/instructions.md
+++ b/exercises/practice/acronym/.docs/instructions.md
@@ -4,5 +4,14 @@ Convert a phrase to its acronym.
Techies love their TLA (Three Letter Acronyms)!
-Help generate some jargon by writing a program that converts a long name
-like Portable Network Graphics to its acronym (PNG).
+Help generate some jargon by writing a program that converts a long name like Portable Network Graphics to its acronym (PNG).
+
+Punctuation is handled as follows: hyphens are word separators (like whitespace); all other punctuation can be removed from the input.
+
+For example:
+
+|Input|Output|
+|-|-|
+|As Soon As Possible|ASAP|
+|Liquid-crystal display|LCD|
+|Thank George It's Friday!|TGIF|
diff --git a/exercises/practice/acronym/.meta/test_template.tera b/exercises/practice/acronym/.meta/test_template.tera
new file mode 100644
index 000000000..c8de609e1
--- /dev/null
+++ b/exercises/practice/acronym/.meta/test_template.tera
@@ -0,0 +1,12 @@
+{% for test in cases %}
+#[test]
+{% if loop.index != 1 -%}
+#[ignore]
+{% endif -%}
+fn {{ test.description | slugify | replace(from="-", to="_") }}() {
+ let input = {{ test.input.phrase | json_encode() }};
+ let output = {{ crate_name }}::{{ fn_names[0] }}(input);
+ let expected = {{ test.expected | json_encode() }};
+ assert_eq!(output, expected);
+}
+{% endfor -%}
diff --git a/exercises/practice/acronym/.meta/tests.toml b/exercises/practice/acronym/.meta/tests.toml
index 157cae14e..6e3277c68 100644
--- a/exercises/practice/acronym/.meta/tests.toml
+++ b/exercises/practice/acronym/.meta/tests.toml
@@ -1,6 +1,13 @@
-# This is an auto-generated file. Regular comments will be removed when this
-# file is regenerated. Regenerating will not touch any manually added keys,
-# so comments can be added in a "comment" key.
+# This is an auto-generated file.
+#
+# Regenerating this file via `configlet sync` will:
+# - Recreate every `description` key/value pair
+# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
+# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
+# - Preserve any other key/value pair
+#
+# As user-added comments (using the # character) will be removed when this file
+# is regenerated, comments can be added via a `comment` key.
[1e22cceb-c5e4-4562-9afe-aef07ad1eaf4]
description = "basic"
diff --git a/exercises/practice/acronym/tests/acronym.rs b/exercises/practice/acronym/tests/acronym.rs
index decf634f9..e90d943a1 100644
--- a/exercises/practice/acronym/tests/acronym.rs
+++ b/exercises/practice/acronym/tests/acronym.rs
@@ -1,84 +1,79 @@
#[test]
-fn empty() {
- assert_eq!(acronym::abbreviate(""), "");
-}
-
-#[test]
-#[ignore]
fn basic() {
- assert_eq!(acronym::abbreviate("Portable Network Graphics"), "PNG");
+ let input = "Portable Network Graphics";
+ let output = acronym::abbreviate(input);
+ let expected = "PNG";
+ assert_eq!(output, expected);
}
#[test]
#[ignore]
fn lowercase_words() {
- assert_eq!(acronym::abbreviate("Ruby on Rails"), "ROR");
-}
-
-#[test]
-#[ignore]
-fn camelcase() {
- assert_eq!(acronym::abbreviate("HyperText Markup Language"), "HTML");
+ let input = "Ruby on Rails";
+ let output = acronym::abbreviate(input);
+ let expected = "ROR";
+ assert_eq!(output, expected);
}
#[test]
#[ignore]
fn punctuation() {
- assert_eq!(acronym::abbreviate("First In, First Out"), "FIFO");
+ let input = "First In, First Out";
+ let output = acronym::abbreviate(input);
+ let expected = "FIFO";
+ assert_eq!(output, expected);
}
#[test]
#[ignore]
fn all_caps_word() {
- assert_eq!(
- acronym::abbreviate("GNU Image Manipulation Program"),
- "GIMP"
- );
-}
-
-#[test]
-#[ignore]
-fn all_caps_word_with_punctuation() {
- assert_eq!(acronym::abbreviate("PHP: Hypertext Preprocessor"), "PHP");
+ let input = "GNU Image Manipulation Program";
+ let output = acronym::abbreviate(input);
+ let expected = "GIMP";
+ assert_eq!(output, expected);
}
#[test]
#[ignore]
fn punctuation_without_whitespace() {
- assert_eq!(
- acronym::abbreviate("Complementary metal-oxide semiconductor"),
- "CMOS"
- );
+ let input = "Complementary metal-oxide semiconductor";
+ let output = acronym::abbreviate(input);
+ let expected = "CMOS";
+ assert_eq!(output, expected);
}
#[test]
#[ignore]
fn very_long_abbreviation() {
- assert_eq!(
- acronym::abbreviate(
- "Rolling On The Floor Laughing So Hard That My Dogs Came Over And Licked Me"
- ),
- "ROTFLSHTMDCOALM"
- );
+ let input = "Rolling On The Floor Laughing So Hard That My Dogs Came Over And Licked Me";
+ let output = acronym::abbreviate(input);
+ let expected = "ROTFLSHTMDCOALM";
+ assert_eq!(output, expected);
}
#[test]
#[ignore]
fn consecutive_delimiters() {
- assert_eq!(
- acronym::abbreviate("Something - I made up from thin air"),
- "SIMUFTA"
- );
+ let input = "Something - I made up from thin air";
+ let output = acronym::abbreviate(input);
+ let expected = "SIMUFTA";
+ assert_eq!(output, expected);
}
#[test]
#[ignore]
fn apostrophes() {
- assert_eq!(acronym::abbreviate("Halley's Comet"), "HC");
+ let input = "Halley's Comet";
+ let output = acronym::abbreviate(input);
+ let expected = "HC";
+ assert_eq!(output, expected);
}
#[test]
#[ignore]
fn underscore_emphasis() {
- assert_eq!(acronym::abbreviate("The Road _Not_ Taken"), "TRNT");
+ let input = "The Road _Not_ Taken";
+ let output = acronym::abbreviate(input);
+ let expected = "TRNT";
+ assert_eq!(output, expected);
}
diff --git a/exercises/practice/allergies/.approaches/introduction.md b/exercises/practice/allergies/.approaches/introduction.md
index fc60fb36a..80a1f732a 100644
--- a/exercises/practice/allergies/.approaches/introduction.md
+++ b/exercises/practice/allergies/.approaches/introduction.md
@@ -8,7 +8,6 @@ Another approach can be to store the `Allergen` values as a [`u32`][u32] and onl
Something to keep in mind is to leverage [bitwise][bitwise] operations to implement the logic.
-
## Approach: Create `Vec` on `new()`
```rust
@@ -104,11 +103,11 @@ impl Allergies {
pub fn new(n: u32) -> Allergies {
Allergies { allergens: n }
}
-
+
pub fn is_allergic_to(&self, allergen: &Allergen) -> bool {
self.allergens & *allergen as u32 != 0
}
-
+
pub fn allergies(&self) -> Vec {
ALLERGENS
.iter()
diff --git a/exercises/practice/allergies/.approaches/vec-when-requested/content.md b/exercises/practice/allergies/.approaches/vec-when-requested/content.md
index 8ea98b250..c3bfce33c 100644
--- a/exercises/practice/allergies/.approaches/vec-when-requested/content.md
+++ b/exercises/practice/allergies/.approaches/vec-when-requested/content.md
@@ -32,11 +32,11 @@ impl Allergies {
pub fn new(n: u32) -> Allergies {
Allergies { allergens: n }
}
-
+
pub fn is_allergic_to(&self, allergen: &Allergen) -> bool {
self.allergens & *allergen as u32 != 0
}
-
+
pub fn allergies(&self) -> Vec {
ALLERGENS
.iter()
@@ -69,7 +69,7 @@ The `new()` method sets its `allergens` field to the `u232` value passed in.
The `is_allergic_to()` method uses the [bitwise AND operator][bitand] (`&`) to compare the `Allergen` passed in with the `allergens` `u32` field.
The dereferenced `Allergen` passed in is [cast][cast] to a `u32` for the purpose of comparison with the `allergens` `u32` value.
-The method returns if the comparison is not `0`.
+The method returns if the comparison is not `0`.
If the comparison is not `0`, then the `allergens` field contains the value of the `Allergen`, and `true` is returned.
For example, if the `allergens` field is decimal `3`, it is binary `11`.
diff --git a/exercises/practice/binary-search/.approaches/looping/content.md b/exercises/practice/binary-search/.approaches/looping/content.md
index 2a31a7bf5..dc598f3b6 100644
--- a/exercises/practice/binary-search/.approaches/looping/content.md
+++ b/exercises/practice/binary-search/.approaches/looping/content.md
@@ -39,7 +39,7 @@ The `T` is constrained to be anything which implements the [`Ord`][ord] trait, w
So, the `key` is of type `T` (orderable), and the `array` is of type `U` (a reference to an indexable container of orderable values
of the same type as the `key`.)
-Although `array` is defined as generic type `U`, which is constrained to be of type `AsRef`,
+Although `array` is defined as generic type `U`, which is constrained to be of type `AsRef`,
the [`as_ref()`][asref] method is used to get the reference to the actual type.
Without it, the compiler would complain that "no method named `len` found for type parameter `U` in the current scope" and
"cannot index into a value of type `U`".
@@ -54,6 +54,7 @@ The [`cmp()`][cmp] method of the `Ord` trait is used to compare that element val
Since the element is a reference, the `key` must also be referenced.
The [`match`][match] arms each use a value from the `Ordering` enum.
+
- If the midpoint element value equals the `key`, then the midpoint is returned from the function wrapped in a [`Some`][some].
- If the midpoint element value is less than the `key`, then the `left` value is adjusted to be one to the right of the midpoint.
- If the midpoint element value is greater than the `key`, then the `right` value is adjusted to be the midpoint.
diff --git a/exercises/practice/binary-search/.approaches/recursion/content.md b/exercises/practice/binary-search/.approaches/recursion/content.md
index f7a5facca..dcae57b84 100644
--- a/exercises/practice/binary-search/.approaches/recursion/content.md
+++ b/exercises/practice/binary-search/.approaches/recursion/content.md
@@ -39,7 +39,7 @@ of the same type as the `key`.)
Since slices of the `array` will keep getting shorter with each recursive call to itself, `find_rec()` has an `offset` parameter
to keep track of the actual midpoint as it relates to the original `array`.
-Although `array` is defined as generic type `U`, which is constrained to be of type `AsRef`,
+Although `array` is defined as generic type `U`, which is constrained to be of type `AsRef`,
the [`as_ref()`][asref] method is used to get the reference to the actual type.
Without it, the compiler would complain that "no method named `len` found for type parameter `U` in the current scope" and
"cannot index into a value of type `U`".
@@ -51,13 +51,14 @@ The [`cmp()`][cmp] method of the `Ord` trait is used to compare that element val
Since the element is a reference, the `key` must also be referenced.
The [`match`][match] arms each use a value from the `Ordering` enum.
+
- If the midpoint element value equals the `key`, then the midpoint plus the offset is returned from the function wrapped in a [`Some`][some].
- If the midpoint element value is less than the `key`, then `find_rec()` calls itself,
-passing a slice of the `array` from the element to the right of the midpoint through the end of the `array`.
-The offset is adjusted to be itself plus the midpoint plus `1`.
+ passing a slice of the `array` from the element to the right of the midpoint through the end of the `array`.
+ The offset is adjusted to be itself plus the midpoint plus `1`.
- If the midpoint element value is greater than the `key`, then `find_rec()` calls itself,
-passing a slice of the `array` from the beginning up to but not including the midpoint element.
-The offset remains as is.
+ passing a slice of the `array` from the beginning up to but not including the midpoint element.
+ The offset remains as is.
While the element value is not equal to the `key`, `find_rec()` keeps calling itself while halving the number of elements being searched,
until either the `key` is found, or, if it is not in the `array`, the `array` is whittled down to empty.
diff --git a/exercises/practice/collatz-conjecture/.docs/instructions.md b/exercises/practice/collatz-conjecture/.docs/instructions.md
index 9a6de68c5..6eec8560e 100644
--- a/exercises/practice/collatz-conjecture/.docs/instructions.md
+++ b/exercises/practice/collatz-conjecture/.docs/instructions.md
@@ -7,8 +7,8 @@ odd, multiply n by 3 and add 1 to get 3n + 1. Repeat the process indefinitely.
The conjecture states that no matter which number you start with, you will
always reach 1 eventually.
-But sometimes the number grow significantly before it reaches 1.
-This can lead to an integer overflow and makes the algorithm unsolvable
+But sometimes the number grow significantly before it reaches 1.
+This can lead to an integer overflow and makes the algorithm unsolvable
within the range of a number in u64.
Given a number n, return the number of steps required to reach 1.
diff --git a/exercises/practice/dot-dsl/.docs/instructions.append.md b/exercises/practice/dot-dsl/.docs/instructions.append.md
index f2bb96c02..33fd9f817 100644
--- a/exercises/practice/dot-dsl/.docs/instructions.append.md
+++ b/exercises/practice/dot-dsl/.docs/instructions.append.md
@@ -1,7 +1,7 @@
# Builder pattern
This exercise expects you to build several structs using `builder pattern`.
-In short, this pattern allows you to split the construction function of your struct, that contains a lot of arguments, into
+In short, this pattern allows you to split the construction function of your struct, that contains a lot of arguments, into
several separate functions. This approach gives you the means to make compact but highly-flexible struct construction and
configuration.
You can read more about it on the [following page](https://doc.rust-lang.org/1.0.0/style/ownership/builders.html).
diff --git a/exercises/practice/isogram/.approaches/filter-all/content.md b/exercises/practice/isogram/.approaches/filter-all/content.md
index 5df0b0429..43e73fd5e 100644
--- a/exercises/practice/isogram/.approaches/filter-all/content.md
+++ b/exercises/practice/isogram/.approaches/filter-all/content.md
@@ -23,18 +23,19 @@ let mut hs = std::collections::HashSet::new();
```
After the `HashSet` is instantiated, a series of functions are chained from the `candidate` `&str`.
+
- Since all of the characters are [ASCII][ascii], they can be iterated with the [`bytes`][bytes] method.
-Each byte is iterated as a [`u8`][u8], which is an unsigned 8-bit integer.
+ Each byte is iterated as a [`u8`][u8], which is an unsigned 8-bit integer.
- The [`filter`][filter] method [borrows][borrow] each byte as a [reference][reference] to a `u8` (`&u8`).
-Inside of its [closure][closure] it tests each byte to see if it [`is_ascii_alphabetic`][is-ascii-alphabetic].
-Only bytes which are ASCII letters will survive the `filter` to be passed on to the [`map`][map] method.
+ Inside of its [closure][closure] it tests each byte to see if it [`is_ascii_alphabetic`][is-ascii-alphabetic].
+ Only bytes which are ASCII letters will survive the `filter` to be passed on to the [`map`][map] method.
- The `map` method calls [`to_ascii_lowercase`][to-ascii-lowercase] on each byte.
- Each lowercased byte is then tested by the [`all`][all] method by using the [`insert`][insert] method of `HashSet`.
-`all` will return `true` if every call to `insert` returns true.
-If a call to `insert` returns `false` then `all` will "short-circuit" and immediately return `false`.
-The `insert` method returns whether the value is _newly_ inserted.
-So, for the word `"alpha"`, `insert` will return `true` when the first `a` is inserted,
-but will return `false` when the second `a` is inserted.
+ `all` will return `true` if every call to `insert` returns true.
+ If a call to `insert` returns `false` then `all` will "short-circuit" and immediately return `false`.
+ The `insert` method returns whether the value is _newly_ inserted.
+ So, for the word `"alpha"`, `insert` will return `true` when the first `a` is inserted,
+ but will return `false` when the second `a` is inserted.
## Refactoring
@@ -53,7 +54,7 @@ candidate
However, changing the case of all characters in a `str` raised the average benchmark a few nanoseconds.
It is a bit faster to `filter` out non-ASCII letters and to change the case of each surviving byte.
-Since the performance is fairly close, either may be prefered.
+Since the performance is fairly close, either may be prefered.
### using `filter_map`
diff --git a/exercises/practice/ocr-numbers/.docs/instructions.md b/exercises/practice/ocr-numbers/.docs/instructions.md
index 4086329bd..a246b898a 100644
--- a/exercises/practice/ocr-numbers/.docs/instructions.md
+++ b/exercises/practice/ocr-numbers/.docs/instructions.md
@@ -40,10 +40,10 @@ Update your program to recognize multi-character binary strings, replacing garbl
Update your program to recognize all numbers 0 through 9, both individually and as part of a larger string.
```text
- _
+ _
_|
-|_
-
+|_
+
```
Is converted to "2"
@@ -62,18 +62,18 @@ Is converted to "1234567890"
Update your program to handle multiple numbers, one per line. When converting several lines, join the lines with commas.
```text
- _ _
+ _ _
| _| _|
||_ _|
-
- _ _
-|_||_ |_
+
+ _ _
+|_||_ |_
| _||_|
-
- _ _ _
+
+ _ _ _
||_||_|
||_| _|
-
+
```
Is converted to "123,456,789"
diff --git a/exercises/practice/rna-transcription/.docs/instructions.append.md b/exercises/practice/rna-transcription/.docs/instructions.append.md
index ae0f7abed..25fc579e1 100644
--- a/exercises/practice/rna-transcription/.docs/instructions.append.md
+++ b/exercises/practice/rna-transcription/.docs/instructions.append.md
@@ -9,5 +9,5 @@ string has a valid RNA string, we don't need to return a `Result`/`Option` from
This explains the type signatures you will see in the tests.
The return types of both `DNA::new()` and `RNA::new()` are `Result`,
-where the error type `usize` represents the index of the first invalid character
+where the error type `usize` represents the index of the first invalid character
(char index, not utf8).
diff --git a/exercises/practice/secret-handshake/.approaches/introduction.md b/exercises/practice/secret-handshake/.approaches/introduction.md
index 7c7d34ff8..ff9c1007e 100644
--- a/exercises/practice/secret-handshake/.approaches/introduction.md
+++ b/exercises/practice/secret-handshake/.approaches/introduction.md
@@ -20,7 +20,7 @@ pub fn actions(n: u8) -> Vec<&'static str> {
_ => (3, -1, -1),
};
let mut output: Vec<&'static str> = Vec::new();
-
+
loop {
if action == end {
break;
diff --git a/exercises/practice/secret-handshake/.approaches/iterate-once/content.md b/exercises/practice/secret-handshake/.approaches/iterate-once/content.md
index f6430f2ab..e4c686646 100644
--- a/exercises/practice/secret-handshake/.approaches/iterate-once/content.md
+++ b/exercises/practice/secret-handshake/.approaches/iterate-once/content.md
@@ -10,7 +10,7 @@ pub fn actions(n: u8) -> Vec<&'static str> {
_ => (3, -1, -1),
};
let mut output: Vec<&'static str> = Vec::new();
-
+
loop {
if action == end {
break;
@@ -40,12 +40,14 @@ The [bitwise AND operator][bitand] is used to check if the input number contains
For example, if the number passed in is `19`, which is `10011` in binary, then it is ANDed with `16`, which is `10000` in binary.
The `1` in `10000` is also at the same position in `10011`, so the two values ANDed will not be `0`.
+
- `10011` AND
- `10000` =
- `10000`
If the number passed in is `3`, which is `00011` in binary, then it is ANDed with `16`, which is `10000` in binary.
The `1` in `10000` is not at the same position in `00011`, so the two values ANDed will be `0`.
+
- `00011` AND
- `10000` =
- `00000`
diff --git a/exercises/practice/simple-linked-list/.approaches/do-not-keep-track-of-length/content.md b/exercises/practice/simple-linked-list/.approaches/do-not-keep-track-of-length/content.md
index 24baa3232..55faa7807 100644
--- a/exercises/practice/simple-linked-list/.approaches/do-not-keep-track-of-length/content.md
+++ b/exercises/practice/simple-linked-list/.approaches/do-not-keep-track-of-length/content.md
@@ -23,11 +23,11 @@ impl SimpleLinkedList {
pub fn new() -> Self {
Self { head: None }
}
-
+
pub fn is_empty(&self) -> bool {
self.head.is_none()
}
-
+
pub fn len(&self) -> usize {
let mut current_node = &self.head;
let mut size = 0;
@@ -37,12 +37,12 @@ impl SimpleLinkedList {
}
size
}
-
+
pub fn push(&mut self, element: T) {
let node = Box::new(Node::new(element, self.head.take()));
self.head = Some(node);
}
-
+
pub fn pop(&mut self) -> Option {
if self.head.is_some() {
let head_node = self.head.take().unwrap();
@@ -52,11 +52,11 @@ impl SimpleLinkedList {
None
}
}
-
+
pub fn peek(&self) -> Option<&T> {
self.head.as_ref().map(|head| &(head.data))
}
-
+
pub fn rev(self) -> SimpleLinkedList {
let mut list = SimpleLinkedList::new();
let mut cur_node = self.head;
diff --git a/exercises/practice/simple-linked-list/.approaches/introduction.md b/exercises/practice/simple-linked-list/.approaches/introduction.md
index 345176cbf..ea38f326f 100644
--- a/exercises/practice/simple-linked-list/.approaches/introduction.md
+++ b/exercises/practice/simple-linked-list/.approaches/introduction.md
@@ -7,7 +7,7 @@ Another approach is to calculate the length every time it is asked for.
## General guidance
One thing to keep in mind is to not mutate the list when it is not necessary.
-For instance, if you find yourself using `mut self` for `rev()` or `into()`, that is an indication that the list is being mutated when it is not necessary.
+For instance, if you find yourself using `mut self` for `rev()` or `into()`, that is an indication that the list is being mutated when it is not necessary.
A well-known treatment of writing linked lists in Rust is [`Learn Rust With Entirely Too Many Linked Lists`][too-many-lists].
@@ -132,11 +132,11 @@ impl SimpleLinkedList {
pub fn new() -> Self {
Self { head: None }
}
-
+
pub fn is_empty(&self) -> bool {
self.head.is_none()
}
-
+
pub fn len(&self) -> usize {
let mut current_node = &self.head;
let mut size = 0;
@@ -146,12 +146,12 @@ impl SimpleLinkedList {
}
size
}
-
+
pub fn push(&mut self, element: T) {
let node = Box::new(Node::new(element, self.head.take()));
self.head = Some(node);
}
-
+
pub fn pop(&mut self) -> Option {
if self.head.is_some() {
let head_node = self.head.take().unwrap();
@@ -161,11 +161,11 @@ impl SimpleLinkedList {
None
}
}
-
+
pub fn peek(&self) -> Option<&T> {
self.head.as_ref().map(|head| &(head.data))
}
-
+
pub fn rev(self) -> SimpleLinkedList {
let mut list = SimpleLinkedList::new();
let mut cur_node = self.head;
diff --git a/exercises/practice/simple-linked-list/.docs/instructions.append.md b/exercises/practice/simple-linked-list/.docs/instructions.append.md
index eabb3266c..8803c5703 100644
--- a/exercises/practice/simple-linked-list/.docs/instructions.append.md
+++ b/exercises/practice/simple-linked-list/.docs/instructions.append.md
@@ -1,23 +1,30 @@
# Implementation Hints
-Do not implement the struct `SimpleLinkedList` as a wrapper around a `Vec`. Instead, allocate nodes on the heap.
+Do not implement the struct `SimpleLinkedList` as a wrapper around a `Vec`. Instead, allocate nodes on the heap.
+
This might be implemented as:
+
```
pub struct SimpleLinkedList {
head: Option>>,
}
```
-The `head` field points to the first element (Node) of this linked list.
+
+The `head` field points to the first element (Node) of this linked list.
+
This implementation also requires a struct `Node` with the following fields:
+
```
struct Node {
data: T,
next: Option>>,
}
```
-`data` contains the stored data, and `next` points to the following node (if available) or None.
+
+`data` contains the stored data, and `next` points to the following node (if available) or None.
## Why `Option>>` and not just `Option>`?
+
Try it on your own. You will get the following error.
```
@@ -26,8 +33,8 @@ Try it on your own. You will get the following error.
...
| next: Option>,
| --------------------- recursive without indirection
- ```
+```
- The problem is that at compile time the size of next must be known.
- Since `next` is recursive ("a node has a node has a node..."), the compiler does not know how much memory is to be allocated.
- In contrast, [Box](https://doc.rust-lang.org/std/boxed/) is a heap pointer with a defined size.
+The problem is that at compile time the size of next must be known.
+Since `next` is recursive ("a node has a node has a node..."), the compiler does not know how much memory is to be allocated.
+In contrast, [Box](https://doc.rust-lang.org/std/boxed/) is a heap pointer with a defined size.
diff --git a/exercises/practice/space-age/.docs/instructions.append.md b/exercises/practice/space-age/.docs/instructions.append.md
index 977d1b6b4..cc76cdff4 100644
--- a/exercises/practice/space-age/.docs/instructions.append.md
+++ b/exercises/practice/space-age/.docs/instructions.append.md
@@ -4,14 +4,13 @@ Some Rust topics you may want to read about while solving this problem:
- Traits, both the From trait and implementing your own traits
- Default method implementations for traits
-- Macros, the use of a macro could reduce boilerplate and increase readability
- for this exercise. For instance,
+- Macros, the use of a macro could reduce boilerplate and increase readability
+ for this exercise. For instance,
[a macro can implement a trait for multiple types at once](https://stackoverflow.com/questions/39150216/implementing-a-trait-for-multiple-types-at-once),
though it is fine to implement `years_during` in the Planet trait itself. A macro could
- define both the structs and their implementations. Info to get started with macros can
+ define both the structs and their implementations. Info to get started with macros can
be found at:
-
+
- [The Macros chapter in The Rust Programming Language](https://doc.rust-lang.org/stable/book/ch19-06-macros.html)
- [an older version of the Macros chapter with helpful detail](https://doc.rust-lang.org/1.30.0/book/first-edition/macros.html)
- [Rust By Example](https://doc.rust-lang.org/stable/rust-by-example/macros.html)
-
diff --git a/justfile b/justfile
new file mode 100644
index 000000000..eb2895877
--- /dev/null
+++ b/justfile
@@ -0,0 +1,28 @@
+_default:
+ just --list --unsorted
+
+# configlet wrapper, uses problem-specifications submodule
+configlet *args="":
+ @[ -f bin/configlet ] || bin/fetch-configlet
+ ./bin/configlet {{ args }}
+
+# simulate CI locally (WIP)
+test:
+ just configlet lint
+ ./bin/lint_markdown.sh
+ # TODO shellcheck
+ ./bin/check_exercises.sh
+ ./bin/ensure_stubs_compile.sh
+ cd rust-tooling && cargo test
+ # TODO format exercises
+
+add-practice-exercise:
+ cd rust-tooling && cargo run --quiet --bin generate_exercise
+
+update-practice-exercise:
+ cd rust-tooling && cargo run --quiet --bin generate_exercise update
+
+# TODO remove. resets result of add-practice-exercise.
+clean:
+ git restore config.json exercises/practice
+ git clean -- exercises/practice
diff --git a/problem-specifications b/problem-specifications
new file mode 160000
index 000000000..d2229dedf
--- /dev/null
+++ b/problem-specifications
@@ -0,0 +1 @@
+Subproject commit d2229dedfa6c6a6bb7d98dc49548d9ae06d0a848
diff --git a/rust-tooling/Cargo.lock b/rust-tooling/Cargo.lock
new file mode 100644
index 000000000..ee7edff04
--- /dev/null
+++ b/rust-tooling/Cargo.lock
@@ -0,0 +1,1106 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bstr"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
+
+[[package]]
+name = "cc"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "num-traits",
+ "windows-targets",
+]
+
+[[package]]
+name = "chrono-tz"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1369bc6b9e9a7dfdae2055f6ec151fe9c554a9d23d357c0237cee2e25eaabb7"
+dependencies = [
+ "chrono",
+ "chrono-tz-build",
+ "phf",
+]
+
+[[package]]
+name = "chrono-tz-build"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2f5ebdc942f57ed96d560a6d1a459bae5851102a25d5bf89dc04ae453e31ecf"
+dependencies = [
+ "parse-zoneinfo",
+ "phf",
+ "phf_codegen",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crossterm"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "libc",
+ "mio",
+ "parking_lot",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "deunicode"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d95203a6a50906215a502507c0f879a0ce7ff205a6111e2db2a5ef8e4bb92e43"
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555"
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "exercism_tooling"
+version = "0.1.0"
+dependencies = [
+ "convert_case",
+ "glob",
+ "ignore",
+ "inquire",
+ "once_cell",
+ "serde",
+ "serde_json",
+ "tera",
+ "uuid",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
+[[package]]
+name = "globset"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "fnv",
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "globwalk"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
+dependencies = [
+ "bitflags",
+ "ignore",
+ "walkdir",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+
+[[package]]
+name = "humansize"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
+dependencies = [
+ "libm",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ignore"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492"
+dependencies = [
+ "globset",
+ "lazy_static",
+ "log",
+ "memchr",
+ "regex",
+ "same-file",
+ "thread_local",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "inquire"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c33e7c1ddeb15c9abcbfef6029d8e29f69b52b6d6c891031b88ed91b5065803b"
+dependencies = [
+ "bitflags",
+ "crossterm",
+ "dyn-clone",
+ "lazy_static",
+ "newline-converter",
+ "thiserror",
+ "unicode-segmentation",
+ "unicode-width",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
+
+[[package]]
+name = "js-sys"
+version = "0.3.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+
+[[package]]
+name = "libm"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4"
+
+[[package]]
+name = "lock_api"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+
+[[package]]
+name = "memchr"
+version = "2.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
+
+[[package]]
+name = "mio"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys",
+]
+
+[[package]]
+name = "newline-converter"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
+name = "parse-zoneinfo"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
+dependencies = [
+ "regex",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
+
+[[package]]
+name = "pest"
+version = "2.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7a4d085fd991ac8d5b05a147b437791b4260b76326baf0fc60cf7c9c27ecd33"
+dependencies = [
+ "memchr",
+ "thiserror",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2bee7be22ce7918f641a33f08e3f43388c7656772244e2bbb2477f44cc9021a"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1511785c5e98d79a05e8a6bc34b4ac2168a0e3e92161862030ad84daa223141"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b42f0394d3123e33353ca5e1e89092e533d2cc490389f2bd6131c43c634ebc5f"
+dependencies = [
+ "once_cell",
+ "pest",
+ "sha2",
+]
+
+[[package]]
+name = "phf"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
+dependencies = [
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
+
+[[package]]
+name = "ryu"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "serde"
+version = "1.0.188"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.188"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "signal-hook"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
+[[package]]
+name = "slug"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
+dependencies = [
+ "deunicode",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
+
+[[package]]
+name = "syn"
+version = "2.0.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tera"
+version = "1.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8"
+dependencies = [
+ "chrono",
+ "chrono-tz",
+ "globwalk",
+ "humansize",
+ "lazy_static",
+ "percent-encoding",
+ "pest",
+ "pest_derive",
+ "rand",
+ "regex",
+ "serde",
+ "serde_json",
+ "slug",
+ "unic-segment",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "typenum"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
+
+[[package]]
+name = "unic-char-property"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
+dependencies = [
+ "unic-char-range",
+]
+
+[[package]]
+name = "unic-char-range"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
+
+[[package]]
+name = "unic-common"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
+
+[[package]]
+name = "unic-segment"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
+dependencies = [
+ "unic-ucd-segment",
+]
+
+[[package]]
+name = "unic-ucd-segment"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-version"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
+dependencies = [
+ "unic-common",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "uuid"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "walkdir"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
diff --git a/rust-tooling/Cargo.toml b/rust-tooling/Cargo.toml
new file mode 100644
index 000000000..a60bfb872
--- /dev/null
+++ b/rust-tooling/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "exercism_tooling"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+convert_case = "0.6.0"
+glob = "0.3.1"
+ignore = "0.4.20"
+inquire = "0.6.2"
+once_cell = "1.18.0"
+serde = { version = "1.0.188", features = ["derive"] }
+serde_json = { version = "1.0.105", features = ["preserve_order"] }
+tera = "1.19.1"
+uuid = { version = "1.4.1", features = ["v4"] }
diff --git a/rust-tooling/src/bin/generate_exercise.rs b/rust-tooling/src/bin/generate_exercise.rs
new file mode 100644
index 000000000..aec5db1e1
--- /dev/null
+++ b/rust-tooling/src/bin/generate_exercise.rs
@@ -0,0 +1,219 @@
+use std::path::PathBuf;
+
+use convert_case::{Case, Casing};
+use exercism_tooling::{
+ exercise_generation, fs_utils,
+ track_config::{self, TRACK_CONFIG},
+};
+use glob::glob;
+use inquire::{validator::Validation, Select, Text};
+
+enum Difficulty {
+ Easy,
+ Medium,
+ // I'm not sure why there are two medium difficulties
+ Medium2,
+ Hard,
+}
+
+impl From for u8 {
+ fn from(difficulty: Difficulty) -> Self {
+ match difficulty {
+ Difficulty::Easy => 1,
+ Difficulty::Medium => 4,
+ Difficulty::Medium2 => 7,
+ Difficulty::Hard => 10,
+ }
+ }
+}
+
+impl std::fmt::Display for Difficulty {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Difficulty::Easy => write!(f, "Easy (1)"),
+ Difficulty::Medium => write!(f, "Medium (4)"),
+ Difficulty::Medium2 => write!(f, "Medium (7)"),
+ Difficulty::Hard => write!(f, "Hard (10)"),
+ }
+ }
+}
+
+fn main() {
+ fs_utils::cd_into_repo_root();
+
+ let is_update = std::env::args().any(|arg| arg == "update");
+
+ let slug = if is_update {
+ ask_for_exercise_to_update()
+ } else {
+ add_entry_to_track_config()
+ };
+
+ make_configlet_generate_what_it_can(&slug);
+
+ generate_exercise_files(&slug, is_update);
+}
+
+fn ask_for_exercise_to_update() -> String {
+ let implemented_exercises = glob("exercises/practice/*")
+ .unwrap()
+ .filter_map(Result::ok)
+ .map(|path| path.file_name().unwrap().to_str().unwrap().to_string())
+ .collect::>();
+
+ Select::new(
+ "Which exercise would you like to update?",
+ implemented_exercises,
+ )
+ .prompt()
+ .unwrap()
+}
+
+/// Interactively prompts the user for required fields in the track config
+/// and writes the answers to config.json.
+/// Returns slug.
+fn add_entry_to_track_config() -> String {
+ let implemented_exercises = glob("exercises/concept/*")
+ .unwrap()
+ .chain(glob("exercises/practice/*").unwrap())
+ .filter_map(Result::ok)
+ .map(|path| path.file_name().unwrap().to_str().unwrap().to_string())
+ .collect::>();
+
+ let unimplemented_with_spec = glob("problem-specifications/exercises/*")
+ .unwrap()
+ .filter_map(Result::ok)
+ .map(|path| path.file_name().unwrap().to_str().unwrap().to_string())
+ .filter(|e| !implemented_exercises.contains(e))
+ .collect::>();
+
+ println!("(suggestions are from problem-specifications)");
+ let slug = Text::new("What's the slug of your exercise?")
+ .with_autocomplete(move |input: &_| {
+ let mut slugs = unimplemented_with_spec.clone();
+ slugs.retain(|e| e.starts_with(input));
+ Ok(slugs)
+ })
+ .with_validator(|input: &str| {
+ if input.is_empty() {
+ Ok(Validation::Invalid("The slug must not be empty.".into()))
+ } else if !input.is_case(Case::Kebab) {
+ Ok(Validation::Invalid(
+ "The slug must be in kebab-case.".into(),
+ ))
+ } else {
+ Ok(Validation::Valid)
+ }
+ })
+ .with_validator(move |input: &str| {
+ if !implemented_exercises.contains(&input.to_string()) {
+ Ok(Validation::Valid)
+ } else {
+ Ok(Validation::Invalid(
+ "An exercise with this slug already exists.".into(),
+ ))
+ }
+ })
+ .prompt()
+ .unwrap();
+
+ let name = Text::new("What's the name of your exercise?")
+ .with_initial_value(&slug.to_case(Case::Title))
+ .prompt()
+ .unwrap();
+
+ let difficulty = Select::::new(
+ "What's the difficulty of your exercise?",
+ vec![
+ Difficulty::Easy,
+ Difficulty::Medium,
+ Difficulty::Medium2,
+ Difficulty::Hard,
+ ],
+ )
+ .prompt()
+ .unwrap()
+ .into();
+
+ let config = track_config::PracticeExercise::new(slug.clone(), name, difficulty);
+
+ let mut track_config = TRACK_CONFIG.clone();
+ track_config.exercises.practice.push(config);
+ let mut new_config = serde_json::to_string_pretty(&track_config)
+ .unwrap()
+ .to_string();
+ new_config += "\n";
+ std::fs::write("config.json", new_config).unwrap();
+
+ println!(
+ "\
+Added your exercise to config.json.
+You can add practices, prerequisites and topics if you like."
+ );
+
+ slug
+}
+
+fn make_configlet_generate_what_it_can(slug: &str) {
+ let status = std::process::Command::new("just")
+ .args([
+ "configlet",
+ "sync",
+ "--update",
+ "--yes",
+ "--docs",
+ "--metadata",
+ "--tests",
+ "include",
+ "--exercise",
+ slug,
+ ])
+ .status()
+ .unwrap();
+ if !status.success() {
+ panic!("configlet sync failed");
+ }
+}
+
+fn generate_exercise_files(slug: &str, is_update: bool) {
+ let fn_names = if is_update {
+ read_fn_names_from_lib_rs(slug)
+ } else {
+ vec!["TODO".to_string()]
+ };
+
+ let exercise = exercise_generation::new(slug, fn_names);
+
+ let exercise_path = PathBuf::from("exercises/practice").join(slug);
+
+ if !is_update {
+ std::fs::write(exercise_path.join(".gitignore"), exercise.gitignore).unwrap();
+ std::fs::write(exercise_path.join("Cargo.toml"), exercise.manifest).unwrap();
+ std::fs::create_dir(exercise_path.join("src")).ok();
+ std::fs::write(exercise_path.join("src/lib.rs"), exercise.lib_rs).unwrap();
+ std::fs::write(exercise_path.join(".meta/example.rs"), exercise.example).unwrap();
+ }
+
+ let template_path = exercise_path.join(".meta/test_template.tera");
+ if std::fs::metadata(&template_path).is_err() {
+ std::fs::write(template_path, exercise.test_template).unwrap();
+ }
+
+ std::fs::create_dir(exercise_path.join("tests")).ok();
+ std::fs::write(
+ exercise_path.join(format!("tests/{slug}.rs")),
+ exercise.tests,
+ )
+ .unwrap();
+}
+
+fn read_fn_names_from_lib_rs(slug: &str) -> Vec {
+ let lib_rs =
+ std::fs::read_to_string(format!("exercises/practice/{}/src/lib.rs", slug)).unwrap();
+
+ lib_rs
+ .split("fn ")
+ .skip(1)
+ .map(|f| f.split_once('(').unwrap().0.to_string())
+ .collect()
+}
diff --git a/rust-tooling/src/default_test_template.tera b/rust-tooling/src/default_test_template.tera
new file mode 100644
index 000000000..39eb4727f
--- /dev/null
+++ b/rust-tooling/src/default_test_template.tera
@@ -0,0 +1,12 @@
+{% for test in cases %}
+#[test]
+{% if loop.index != 1 -%}
+#[ignore]
+{% endif -%}
+fn {{ test.description | slugify | replace(from="-", to="_") }}() {
+ let input = {{ test.input | json_encode() }};
+ let output = {{ crate_name }}::{{ fn_names[0] }}(input);
+ let expected = {{ test.expected | json_encode() }};
+ assert_eq!(output, expected);
+}
+{% endfor -%}
diff --git a/rust-tooling/src/exercise_config.rs b/rust-tooling/src/exercise_config.rs
new file mode 100644
index 000000000..f8b170356
--- /dev/null
+++ b/rust-tooling/src/exercise_config.rs
@@ -0,0 +1,127 @@
+//! This module provides a data structure for exercise configuration stored in
+//! `.meta/config`. It is capable of serializing and deserializing th
+//! configuration, for example with `serde_json`.
+
+use serde::{Deserialize, Serialize};
+use tera::Tera;
+
+use crate::track_config::TRACK_CONFIG;
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct ConceptExercise {
+ pub authors: Vec,
+ pub contributors: Option>,
+ pub files: ConceptFiles,
+ pub icon: Option,
+ pub blurb: String,
+ pub source: Option,
+ pub source_url: Option,
+ pub test_runner: Option,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct ConceptFiles {
+ pub solution: Vec,
+ pub test: Vec,
+ pub exemplar: Vec,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct PracticeExercise {
+ pub authors: Vec,
+ pub contributors: Option>,
+ pub files: PracticeFiles,
+ pub icon: Option,
+ pub blurb: String,
+ pub source: Option,
+ pub source_url: Option,
+ pub test_runner: Option,
+ pub custom: Option,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct PracticeFiles {
+ pub solution: Vec,
+ pub test: Vec,
+ pub example: Vec,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct Custom {
+ #[serde(rename = "allowed-to-not-compile")]
+ pub allowed_to_not_compile: Option,
+ #[serde(rename = "test-in-release-mode")]
+ pub test_in_release_mode: Option,
+ #[serde(rename = "ignore-count-ignores")]
+ pub ignore_count_ignores: Option,
+}
+
+pub fn get_all_concept_exercise_paths() -> impl Iterator- {
+ let crate_dir = env!("CARGO_MANIFEST_DIR");
+
+ TRACK_CONFIG
+ .exercises
+ .concept
+ .iter()
+ .map(move |e| format!("{crate_dir}/../exercises/concept/{}", e.slug))
+}
+
+pub fn get_all_practice_exercise_paths() -> impl Iterator
- {
+ let crate_dir = env!("CARGO_MANIFEST_DIR");
+
+ TRACK_CONFIG
+ .exercises
+ .practice
+ .iter()
+ .map(move |e| format!("{crate_dir}/../exercises/practice/{}", e.slug))
+}
+
+pub fn get_all_exercise_paths() -> impl Iterator
- {
+ get_all_concept_exercise_paths().chain(get_all_practice_exercise_paths())
+}
+
+#[test]
+fn test_deserialize_all() {
+ for path in get_all_concept_exercise_paths() {
+ let config_path = format!("{path}/.meta/config.json");
+ let config_contents = std::fs::read_to_string(config_path).unwrap();
+ let _: ConceptExercise = serde_json::from_str(config_contents.as_str())
+ .expect("should deserialize concept exercise config");
+ }
+ for path in get_all_practice_exercise_paths() {
+ let config_path = format!("{path}/.meta/config.json");
+ let config_contents = std::fs::read_to_string(config_path).unwrap();
+ let _: PracticeExercise = serde_json::from_str(config_contents.as_str())
+ .expect("should deserialize practice exercise config");
+ }
+}
+
+/// Returns the uuids of the tests excluded in .meta/tests.toml
+pub fn get_excluded_tests(slug: &str) -> Vec {
+ let path = std::path::PathBuf::from("exercises/practice")
+ .join(slug)
+ .join(".meta/tests.toml");
+ let contents = std::fs::read_to_string(&path).unwrap();
+
+ let mut excluded_tests = Vec::new();
+
+ // shitty toml parser
+ for case in contents.split("\n[").skip(1) {
+ let (uuid, rest) = case.split_once(']').unwrap();
+ if rest.contains("include = false") {
+ excluded_tests.push(uuid.to_string());
+ }
+ }
+
+ excluded_tests
+}
+
+/// Returns the uuids of the tests excluded in .meta/tests.toml
+pub fn get_test_emplate(slug: &str) -> Option {
+ Some(Tera::new(format!("exercises/practice/{slug}/.meta/*.tera").as_str()).unwrap())
+}
diff --git a/rust-tooling/src/exercise_generation.rs b/rust-tooling/src/exercise_generation.rs
new file mode 100644
index 000000000..028776a61
--- /dev/null
+++ b/rust-tooling/src/exercise_generation.rs
@@ -0,0 +1,104 @@
+use tera::Context;
+
+use crate::{
+ exercise_config::{get_excluded_tests, get_test_emplate},
+ problem_spec::{get_canonical_data, SingleTestCase, TestCase},
+};
+
+pub struct GeneratedExercise {
+ pub gitignore: String,
+ pub manifest: String,
+ pub lib_rs: String,
+ pub example: String,
+ pub test_template: String,
+ pub tests: String,
+}
+
+pub fn new(slug: &str, fn_names: Vec) -> GeneratedExercise {
+ let crate_name = slug.replace('-', "_");
+ let first_fn_name = &fn_names[0];
+
+ GeneratedExercise {
+ gitignore: GITIGNORE.into(),
+ manifest: generate_manifest(&crate_name),
+ lib_rs: generate_lib_rs(&crate_name, first_fn_name),
+ example: generate_example_rs(first_fn_name),
+ test_template: TEST_TEMPLATE.into(),
+ tests: generate_tests(slug, fn_names),
+ }
+}
+
+static GITIGNORE: &str = "\
+/target
+/Cargo.lock
+";
+
+fn generate_manifest(crate_name: &str) -> String {
+ format!(
+ concat!(
+ "[package]\n",
+ "edition = \"2021\"\n",
+ "name = \"{crate_name}\"\n",
+ "version = \"1.0.0\"\n",
+ "\n",
+ "[dependencies]\n",
+ ),
+ crate_name = crate_name
+ )
+}
+
+fn generate_lib_rs(crate_name: &str, fn_name: &str) -> String {
+ format!(
+ concat!(
+ "pub fn {fn_name}(input: TODO) -> TODO {{\n",
+ " todo!(\"use {{input}} to implement {crate_name}\")\n",
+ "}}\n",
+ ),
+ fn_name = fn_name,
+ crate_name = crate_name,
+ )
+}
+
+fn generate_example_rs(fn_name: &str) -> String {
+ format!(
+ concat!(
+ "pub fn {fn_name}(input: TODO) -> TODO {{\n",
+ " TODO\n",
+ "}}\n",
+ ),
+ fn_name = fn_name
+ )
+}
+
+static TEST_TEMPLATE: &str = include_str!("default_test_template.tera");
+
+fn extend_single_cases(single_cases: &mut Vec, cases: Vec) {
+ for case in cases {
+ match case {
+ TestCase::Single { case } => single_cases.push(case),
+ TestCase::Group { cases, .. } => extend_single_cases(single_cases, cases),
+ }
+ }
+}
+
+fn generate_tests(slug: &str, fn_names: Vec) -> String {
+ let cases = get_canonical_data(slug).cases;
+ let excluded_tests = get_excluded_tests(slug);
+ let mut template = get_test_emplate(slug).unwrap();
+ if template.get_template_names().next().is_none() {
+ template
+ .add_raw_template("test_template.tera", TEST_TEMPLATE)
+ .unwrap();
+ }
+
+ let mut single_cases = Vec::new();
+ extend_single_cases(&mut single_cases, cases);
+ single_cases.retain(|case| !excluded_tests.contains(&case.uuid));
+
+ let mut context = Context::new();
+ context.insert("crate_name", &slug.replace('-', "_"));
+ context.insert("fn_names", &fn_names);
+ context.insert("cases", &single_cases);
+
+ template.render("test_template.tera", &context).unwrap().trim_start().into()
+}
diff --git a/rust-tooling/src/fs_utils.rs b/rust-tooling/src/fs_utils.rs
new file mode 100644
index 000000000..6af30496a
--- /dev/null
+++ b/rust-tooling/src/fs_utils.rs
@@ -0,0 +1,12 @@
+//! This module contains utilities for working with the files in this repo.
+
+/// Changes the current working directory to the root of the repository.
+///
+/// This is intended to be used by executables which operate on files
+/// of the repository, so they can use relative paths and still work
+/// when called from anywhere within the repository.
+pub fn cd_into_repo_root() {
+ static RUST_TOOLING_DIR: &str = env!("CARGO_MANIFEST_DIR");
+ let repo_root_dir = std::path::PathBuf::from(RUST_TOOLING_DIR).join("..");
+ std::env::set_current_dir(repo_root_dir).unwrap();
+}
diff --git a/rust-tooling/src/lib.rs b/rust-tooling/src/lib.rs
new file mode 100644
index 000000000..727eaca9a
--- /dev/null
+++ b/rust-tooling/src/lib.rs
@@ -0,0 +1,5 @@
+pub mod problem_spec;
+pub mod exercise_config;
+pub mod track_config;
+pub mod exercise_generation;
+pub mod fs_utils;
diff --git a/rust-tooling/src/problem_spec.rs b/rust-tooling/src/problem_spec.rs
new file mode 100644
index 000000000..073b8ba97
--- /dev/null
+++ b/rust-tooling/src/problem_spec.rs
@@ -0,0 +1,69 @@
+use serde::{Deserialize, Serialize};
+
+/// Remember that this is actually optional, not all exercises
+/// must have a canonical data file in the problem-specifications repo.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct CanonicalData {
+ pub exercise: String,
+ pub comments: Option>,
+ pub cases: Vec,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+#[serde(untagged)]
+pub enum TestCase {
+ Single {
+ #[serde(flatten)]
+ case: SingleTestCase,
+ },
+ Group {
+ description: String,
+ comments: Option>,
+ cases: Vec,
+ },
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct SingleTestCase {
+ pub uuid: String,
+ pub reimplements: Option,
+ pub description: String,
+ pub comments: Option>,
+ pub scenarios: Option>,
+ pub property: String,
+ pub input: serde_json::Value,
+ pub expected: serde_json::Value,
+}
+
+pub fn get_canonical_data(slug: &str) -> CanonicalData {
+ let path = std::path::PathBuf::from("problem-specifications/exercises")
+ .join(slug)
+ .join("canonical-data.json");
+ let contents = std::fs::read_to_string(&path).unwrap();
+ serde_json::from_str(contents.as_str()).unwrap_or_else(|e| {
+ panic!(
+ "should deserialize canonical data for {}: {e}",
+ path.display()
+ )
+ })
+}
+
+#[test]
+fn test_deserialize_canonical_data() {
+ crate::fs_utils::cd_into_repo_root();
+ for entry in ignore::Walk::new("problem-specifications/exercises")
+ .filter_map(|e| e.ok())
+ .filter(|e| e.file_name().to_str().unwrap() == "canonical-data.json")
+ {
+ let contents = std::fs::read_to_string(entry.path()).unwrap();
+ let _: CanonicalData = serde_json::from_str(contents.as_str()).unwrap_or_else(|e| {
+ panic!(
+ "should deserialize canonical data for {}: {e}",
+ entry.path().display()
+ )
+ });
+ }
+}
diff --git a/rust-tooling/src/track_config.rs b/rust-tooling/src/track_config.rs
new file mode 100644
index 000000000..01c425982
--- /dev/null
+++ b/rust-tooling/src/track_config.rs
@@ -0,0 +1,145 @@
+//! This module provides a data structure for the track configuration.
+//! It is capable of serializing and deserializing the configuration,
+//! for example with `serde_json`.
+//!
+//! Some definitions may not be perfectly precise.
+//! Feel free to improve this if need be.
+//! (e.g. replace `String` with an enum of possible values)
+
+use std::collections::HashMap;
+
+use once_cell::sync::Lazy;
+use serde::{Deserialize, Serialize};
+
+pub static TRACK_CONFIG: Lazy = Lazy::new(|| {
+ let config = include_str!("../../config.json");
+ serde_json::from_str(config).expect("should deserialize the track config")
+});
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct TrackConfig {
+ pub language: String,
+ pub slug: String,
+ pub active: bool,
+ pub status: Status,
+ pub blurb: String,
+ pub version: u8,
+ pub online_editor: OnlineEditor,
+ pub test_runner: HashMap,
+ pub files: Files,
+ pub exercises: Exercises,
+ pub concepts: Vec,
+ pub key_features: Vec,
+ pub tags: Vec,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct Status {
+ pub concept_exercises: bool,
+ pub test_runner: bool,
+ pub representer: bool,
+ pub analyzer: bool,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct OnlineEditor {
+ pub indent_style: String,
+ pub indent_size: u8,
+ pub highlightjs_language: String,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct Files {
+ pub solution: Vec,
+ pub test: Vec,
+ pub example: Vec,
+ pub exemplar: Vec,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct Exercises {
+ pub concept: Vec,
+ pub practice: Vec,
+ pub foregone: Vec,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum ConceptExerciseStatus {
+ Active,
+ Wip,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct ConceptExercise {
+ pub slug: String,
+ pub uuid: String,
+ pub name: String,
+ pub difficulty: u8,
+ pub concepts: Vec,
+ pub prerequisites: Vec,
+ pub status: ConceptExerciseStatus,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum PracticeExerciseStatus {
+ Deprecated,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct PracticeExercise {
+ pub slug: String,
+ pub name: String,
+ pub uuid: String,
+ pub practices: Vec,
+ pub prerequisites: Vec,
+ pub difficulty: u8,
+ pub topics: Vec,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub status: Option,
+}
+
+impl PracticeExercise {
+ pub fn new(slug: String, name: String, difficulty: u8) -> Self {
+ Self {
+ slug,
+ name,
+ uuid: uuid::Uuid::new_v4().to_string(),
+ practices: Vec::new(),
+ prerequisites: Vec::new(),
+ difficulty,
+ topics: Vec::new(),
+ status: None,
+ }
+ }
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct ConceptConfig {
+ pub uuid: String,
+ pub slug: String,
+ pub name: String,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct KeyFeature {
+ pub icon: String,
+ pub title: String,
+ pub content: String,
+}
+
+#[test]
+fn test_deserialize() {
+ // force deserialization of lazy static
+ assert!(TRACK_CONFIG.active, "should deserialize track config");
+}
diff --git a/rust-tooling/tests/bash_script_conventions.rs b/rust-tooling/tests/bash_script_conventions.rs
new file mode 100644
index 000000000..ff95feb88
--- /dev/null
+++ b/rust-tooling/tests/bash_script_conventions.rs
@@ -0,0 +1,72 @@
+use std::path::PathBuf;
+
+use convert_case::{Case, Casing};
+use exercism_tooling::fs_utils;
+
+/// Runs a function for each bash script in the bin directory.
+/// The function is passed the path of the script.
+fn for_all_scripts(f: fn(PathBuf)) {
+ fs_utils::cd_into_repo_root();
+
+ for entry in std::fs::read_dir("bin").unwrap() {
+ f(entry.unwrap().path())
+ }
+}
+
+#[test]
+fn test_file_extension() {
+ for_all_scripts(|path| {
+ let file_name = path.file_name().unwrap().to_str().unwrap();
+
+ // exceptions
+ if file_name == "fetch-configlet" || file_name == "configlet" {
+ return;
+ }
+
+ assert!(
+ file_name.ends_with(".sh"),
+ "name of '{file_name}' should end with .sh"
+ );
+ })
+}
+
+#[test]
+fn test_snake_case_name() {
+ for_all_scripts(|path| {
+ let file_name = path
+ .file_name()
+ .unwrap()
+ .to_str()
+ .unwrap()
+ .trim_end_matches(".sh");
+
+ // fetch-configlet comes from upstream, we don't control its name
+ if file_name == "fetch-configlet" {
+ return;
+ }
+
+ assert!(
+ file_name.is_case(Case::Snake),
+ "name of '{file_name}' should be snake_case"
+ );
+ })
+}
+
+/// Notably on nixOS and macOS, bash is not installed in `/bin/bash`.
+#[test]
+fn test_portable_shebang() {
+ for_all_scripts(|path| {
+ let file_name = path.file_name().unwrap().to_str().unwrap();
+
+ // not a bash script, but it must be in `bin`
+ if file_name == "configlet" {
+ return;
+ }
+
+ let contents = std::fs::read_to_string(&path).unwrap();
+ assert!(
+ contents.starts_with("#!/usr/bin/env bash"),
+ "'{file_name}' should start with the shebang '#!/usr/bin/env bash'"
+ );
+ })
+}
diff --git a/rust-tooling/tests/count_ignores.rs b/rust-tooling/tests/count_ignores.rs
new file mode 100644
index 000000000..4a78d04ca
--- /dev/null
+++ b/rust-tooling/tests/count_ignores.rs
@@ -0,0 +1,34 @@
+use exercism_tooling::exercise_config::{
+ get_all_concept_exercise_paths, get_all_practice_exercise_paths, PracticeExercise,
+};
+
+fn assert_one_less_ignore_than_tests(path: &str) {
+ let slug = path.split('/').last().unwrap();
+ let test_path = format!("{path}/tests/{slug}.rs");
+ let test_contents = std::fs::read_to_string(test_path).unwrap();
+ let num_tests = test_contents.matches("#[test]").count();
+ let num_ignores = test_contents.matches("#[ignore]").count();
+ assert_eq!(
+ num_tests,
+ num_ignores + 1,
+ "should have one more test than ignore in {slug}"
+ )
+}
+
+#[test]
+fn test_count_ignores() {
+ for path in get_all_concept_exercise_paths() {
+ assert_one_less_ignore_than_tests(&path);
+ }
+ for path in get_all_practice_exercise_paths() {
+ let config_path = format!("{path}/.meta/config.json");
+ let config_contents = std::fs::read_to_string(config_path).unwrap();
+ let config: PracticeExercise = serde_json::from_str(config_contents.as_str()).unwrap();
+ if let Some(custom) = config.custom {
+ if custom.ignore_count_ignores.unwrap_or_default() {
+ continue;
+ }
+ }
+ assert_one_less_ignore_than_tests(&path);
+ }
+}
diff --git a/rust-tooling/tests/difficulties.rs b/rust-tooling/tests/difficulties.rs
new file mode 100644
index 000000000..18c96bc03
--- /dev/null
+++ b/rust-tooling/tests/difficulties.rs
@@ -0,0 +1,18 @@
+//! Make sure exercise difficulties are set correctly
+
+use exercism_tooling::track_config::TRACK_CONFIG;
+
+#[test]
+fn test_difficulties_are_valid() {
+ let mut difficulties = TRACK_CONFIG
+ .exercises
+ .concept
+ .iter()
+ .map(|e| e.difficulty)
+ .chain(TRACK_CONFIG.exercises.practice.iter().map(|e| e.difficulty));
+
+ assert!(
+ difficulties.all(|d| matches!(d, 1 | 4 | 7 | 10)),
+ "exercises must have a difficulty of 1, 4, 7, or 10"
+ )
+}
diff --git a/rust-tooling/tests/no_authors_in_cargo_toml.rs b/rust-tooling/tests/no_authors_in_cargo_toml.rs
new file mode 100644
index 000000000..c8428ce34
--- /dev/null
+++ b/rust-tooling/tests/no_authors_in_cargo_toml.rs
@@ -0,0 +1,16 @@
+use exercism_tooling::exercise_config::get_all_exercise_paths;
+
+/// The package manifest of each exercise should not contain an `authors` field.
+/// The authors are already specified in the track configuration.
+#[test]
+fn test_no_authors_in_cargo_toml() {
+ let cargo_toml_paths = get_all_exercise_paths().map(|p| format!("{p}/Cargo.toml"));
+
+ for path in cargo_toml_paths {
+ let cargo_toml = std::fs::read_to_string(path).unwrap();
+ assert!(
+ !cargo_toml.contains("authors"),
+ "Cargo.toml should not contain an 'authors' field"
+ );
+ }
+}
diff --git a/rust-tooling/tests/no_trailing_whitespace.rs b/rust-tooling/tests/no_trailing_whitespace.rs
new file mode 100644
index 000000000..540c160b9
--- /dev/null
+++ b/rust-tooling/tests/no_trailing_whitespace.rs
@@ -0,0 +1,34 @@
+use std::path::Path;
+
+use exercism_tooling::fs_utils;
+
+fn contains_trailing_whitespace(p: &Path) -> bool {
+ let contents = std::fs::read_to_string(p).unwrap();
+ for line in contents.lines() {
+ if line != line.trim_end() {
+ return true;
+ }
+ }
+ false
+}
+
+#[test]
+fn test_no_trailing_whitespace() {
+ fs_utils::cd_into_repo_root();
+
+ for entry in ignore::Walk::new("./") {
+ let entry = entry.unwrap();
+ if !entry.file_type().is_some_and(|t| t.is_file()) {
+ continue;
+ }
+ let path = entry.path();
+ let ext = path.extension().unwrap_or_default().to_str().unwrap();
+ if matches!(ext, "rs" | "toml" | "md" | "sh") {
+ assert!(
+ !contains_trailing_whitespace(path),
+ "trailing whitespace in {}",
+ path.display()
+ );
+ }
+ }
+}
diff --git a/util/escape_double_quotes/Cargo.lock b/util/escape_double_quotes/Cargo.lock
deleted file mode 100644
index 54ec08296..000000000
--- a/util/escape_double_quotes/Cargo.lock
+++ /dev/null
@@ -1,7 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 3
-
-[[package]]
-name = "escape_double_quotes"
-version = "0.1.0"
diff --git a/util/escape_double_quotes/Cargo.toml b/util/escape_double_quotes/Cargo.toml
deleted file mode 100644
index c61c6ddbc..000000000
--- a/util/escape_double_quotes/Cargo.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-[package]
-name = "escape_double_quotes"
-version = "0.1.0"
-edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
diff --git a/util/escape_double_quotes/build b/util/escape_double_quotes/build
deleted file mode 100755
index b74a56633..000000000
--- a/util/escape_double_quotes/build
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/usr/bin/env bash
-cargo build --release --quiet && cp ./target/release/escape_double_quotes ../../bin/generator-utils && rm -rf ./target
diff --git a/util/escape_double_quotes/src/lib.rs b/util/escape_double_quotes/src/lib.rs
deleted file mode 100644
index e91f3a79b..000000000
--- a/util/escape_double_quotes/src/lib.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-pub mod utils;
-pub use utils::escape_double_quotes::*;
diff --git a/util/escape_double_quotes/src/main.rs b/util/escape_double_quotes/src/main.rs
deleted file mode 100644
index 3890e92bf..000000000
--- a/util/escape_double_quotes/src/main.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-use std::env;
-use std::fs::File;
-use std::io::{self, BufRead, BufReader, BufWriter, Write};
-mod utils;
-use utils::escape_double_quotes::*;
-
-
-fn main() -> io::Result<()> {
- let args: Vec = env::args().collect();
- if args.len() != 2 {
- eprintln!("Usage: {} ", args[0]);
- std::process::exit(1);
- }
-
- let file_path = &args[1];
- let file = File::open(file_path)?;
- let reader = BufReader::new(file);
-
- let stdout = io::stdout();
- let mut writer = BufWriter::new(stdout.lock());
-
- for line in reader.lines() {
- let input = line?;
- let output = escape_double_quotes(&input);
- writeln!(writer, "{}", output)?;
- }
-
- Ok(())
-}
-
diff --git a/util/escape_double_quotes/src/utils/escape_double_quotes.rs b/util/escape_double_quotes/src/utils/escape_double_quotes.rs
deleted file mode 100644
index 83fb7b907..000000000
--- a/util/escape_double_quotes/src/utils/escape_double_quotes.rs
+++ /dev/null
@@ -1,22 +0,0 @@
-pub fn escape_double_quotes(input: &str) -> String {
- let mut output = String::new();
- let mut escape = true;
-
- let mut chars = input.chars().peekable();
- while let Some(char) = chars.next() {
- match char {
- '$' if chars.peek() == Some(&'{') => {
- escape = false;
- output.push(char)
- }
- '}' if chars.peek() == Some(&'$') => {
- escape = true;
- output.push(char)
- }
- '"' if escape => output.push_str("\\\""),
- _ => output.push(char),
- }
- }
-
- output
-}
diff --git a/util/escape_double_quotes/src/utils/mod.rs b/util/escape_double_quotes/src/utils/mod.rs
deleted file mode 100644
index a352b9aaf..000000000
--- a/util/escape_double_quotes/src/utils/mod.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub mod escape_double_quotes;
diff --git a/util/escape_double_quotes/tests/test.rs b/util/escape_double_quotes/tests/test.rs
deleted file mode 100644
index 99e82935e..000000000
--- a/util/escape_double_quotes/tests/test.rs
+++ /dev/null
@@ -1,36 +0,0 @@
-use escape_double_quotes::utils::escape_double_quotes::escape_double_quotes;
-
-#[test]
-fn test_no_double_quotes() {
- let input = "let x = 5;";
- let expected = "let x = 5;";
- assert_eq!(escape_double_quotes(input), expected);
-}
-
-#[test]
-fn test_simple_double_quotes() {
- let input = "let something = \"string\";";
- let expected = "let something = \\\"string\\\";";
- assert_eq!(escape_double_quotes(input), expected);
-}
-
-#[test]
-fn test_braces_with_double_quotes() {
- let input = "let expected = \"${expected | join(\\\"\\n\\\")}$\";";
- let expected = "let expected = \\\"${expected | join(\\\"\\n\\\")}$\\\";";
- assert_eq!(escape_double_quotes(input), expected);
-}
-
-#[test]
-fn test_mixed_double_quotes() {
- let input = "let a = \"value\"; let b = \"${value | filter(\\\"text\\\")}$\";";
- let expected = "let a = \\\"value\\\"; let b = \\\"${value | filter(\\\"text\\\")}$\\\";";
- assert_eq!(escape_double_quotes(input), expected);
-}
-
-#[test]
-fn test_nested_braces() {
- let input = "let nested = \"${outer {inner | escape(\\\"\\n\\\")}}$\";";
- let expected = "let nested = \\\"${outer {inner | escape(\\\"\\n\\\")}}$\\\";";
- assert_eq!(escape_double_quotes(input), expected);
-}
diff --git a/util/ngram/Cargo.lock b/util/ngram/Cargo.lock
deleted file mode 100644
index b704c48b2..000000000
--- a/util/ngram/Cargo.lock
+++ /dev/null
@@ -1,16 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 3
-
-[[package]]
-name = "ngram"
-version = "0.1.0"
-dependencies = [
- "ngrammatic",
-]
-
-[[package]]
-name = "ngrammatic"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c6f2f987e82da7fb8a290e959bba528638bc0e2629e38647591e845ecb5f6fe"
diff --git a/util/ngram/Cargo.toml b/util/ngram/Cargo.toml
deleted file mode 100644
index 5b48fcc38..000000000
--- a/util/ngram/Cargo.toml
+++ /dev/null
@@ -1,9 +0,0 @@
-[package]
-name = "ngram"
-version = "0.1.0"
-edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
-ngrammatic = "0.4.0"
diff --git a/util/ngram/build b/util/ngram/build
deleted file mode 100755
index 8e428879f..000000000
--- a/util/ngram/build
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/usr/bin/env bash
-cargo build --release --quiet && cp ./target/release/ngram ../../bin/generator-utils && rm -rf ./target
diff --git a/util/ngram/src/main.rs b/util/ngram/src/main.rs
deleted file mode 100644
index 156b39cba..000000000
--- a/util/ngram/src/main.rs
+++ /dev/null
@@ -1,26 +0,0 @@
-use ngrammatic::{CorpusBuilder, Pad};
-
-fn main() {
- let mut args = std::env::args();
- let exercises = args.nth(1).expect("Missing exercises argument");
- let slug = args.nth(0).expect("Missing slug argument");
- let exercises: Vec<&str> = exercises
- .split(|c: char| c.is_whitespace() || c == '\n')
- .collect();
- let mut corpus = CorpusBuilder::new().arity(2).pad_full(Pad::Auto).finish();
-
- for exercise in exercises.iter() {
- corpus.add_text(exercise);
- }
-
- if let Some(top_result) = corpus.search(&slug, 0.25).first() {
- println!(
- "{} - There is an exercise with a similar name: '{}' [{:.0}% match]",
- slug,
- top_result.text,
- top_result.similarity * 100.0
- );
- } else {
- println!("Couldn't find any exercise similar to this: {}", slug);
- }
-}