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); - } -}