Skip to content

Commit 29afef9

Browse files
authored
Add script to help add new practice exercise (#1635)
Add a `bin/add_practice_exercise <slug>` script that helps with adding a new practice exercise. The script will: 1. Add an entry to the `config.json`'s `exercises.practice[]` array 2. Create exercism-specific files, like `.meta/config.json` and the documentation 3. Create Rust-specific files, like `Cargo.toml` and `src/lib.rb` 4. Create a tests files with functions for the tests cases as defined in the exercise's canonical data
1 parent a4d8b88 commit 29afef9

37 files changed

+525
-3624
lines changed

.github/workflows/tests.yml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -123,20 +123,12 @@ jobs:
123123
run: ./_test/check_exercises.sh
124124
continue-on-error: ${{ matrix.rust == 'beta' && matrix.deny_warnings == '1' }}
125125

126-
- name: Cargo clean (to prevent previous compilation from unintentionally interfering with later ones)
127-
run: ./_test/cargo_clean_all.sh
128-
129126
- name: Ensure stubs compile
130127
env:
131128
DENYWARNINGS: ${{ matrix.deny_warnings }}
132129
run: ./_test/ensure_stubs_compile.sh
133130
continue-on-error: ${{ matrix.rust == 'beta' && matrix.deny_warnings == '1' }}
134131

135-
- name: Check exercise crate
136-
env:
137-
DENYWARNINGS: ${{ matrix.deny_warnings }}
138-
run: ./_test/check_exercise_crate.sh
139-
continue-on-error: ${{ matrix.rust == 'beta' && matrix.deny_warnings == '1' }}
140132

141133
rustformat:
142134
name: Check Rust Formatting

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,8 @@ bin/configlet
77
bin/configlet.exe
88
bin/exercise
99
bin/exercise.exe
10+
bin/generator-utils/ngram
1011
exercises/*/*/Cargo.lock
1112
exercises/*/*/clippy.log
13+
canonical_data.json
14+
.vscode

_test/cargo_clean_all.sh

Lines changed: 0 additions & 16 deletions
This file was deleted.

_test/check_exercise_crate.sh

Lines changed: 0 additions & 37 deletions
This file was deleted.

bin/add_practice_exercise

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env bash
2+
3+
# shellcheck source=/dev/null
4+
source ./bin/generator-utils/utils.sh
5+
source ./bin/generator-utils/prompts.sh
6+
source ./bin/generator-utils/templates.sh
7+
8+
# Exit if anything fails.
9+
set -euo pipefail
10+
11+
# If argument not provided, print usage and exit
12+
if [ $# -ne 1 ] && [ $# -ne 2 ] && [ $# -ne 3 ]; then
13+
echo "Usage: bin/add_practice_exercise <exercise-slug> [difficulty] [author-github-handle]"
14+
exit 1
15+
fi
16+
17+
# Check if sed is gnu-sed
18+
if ! sed --version | grep -q "GNU sed"; then
19+
echo "GNU sed is required. Please install it and make sure it's in your PATH."
20+
exit 1
21+
fi
22+
23+
# Check if jq and curl are installed
24+
command -v jq >/dev/null 2>&1 || {
25+
echo >&2 "jq is required but not installed. Please install it and make sure it's in your PATH."
26+
exit 1
27+
}
28+
command -v curl >/dev/null 2>&1 || {
29+
echo >&2 "curl is required but not installed. Please install it and make sure it's in your PATH."
30+
exit 1
31+
}
32+
33+
# Check if exercise exists in configlet info or in config.json
34+
check_exercise_existence "$1"
35+
36+
# ==================================================
37+
38+
SLUG="$1"
39+
HAS_CANONICAL_DATA=true
40+
# Fetch canonical data
41+
canonical_json=$(bin/fetch_canonical_data "$SLUG")
42+
43+
if [ "${canonical_json}" == "404: Not Found" ]; then
44+
HAS_CANONICAL_DATA=false
45+
message "warning" "This exercise doesn't have canonical data"
46+
47+
else
48+
echo "$canonical_json" >canonical_data.json
49+
message "success" "Fetched canonical data successfully!"
50+
fi
51+
52+
UNDERSCORED_SLUG=$(dash_to_underscore "$SLUG")
53+
EXERCISE_DIR="exercises/practice/${SLUG}"
54+
EXERCISE_NAME=$(format_exercise_name "$SLUG")
55+
message "info" "Using ${YELLOW}${EXERCISE_NAME}${BLUE} as a default exercise name. You can edit this later in the config.json file"
56+
# using default value for difficulty
57+
EXERCISE_DIFFICULTY=$(validate_difficulty_input "${2:-$(get_exercise_difficulty)}")
58+
message "info" "The exercise difficulty has been set to ${YELLOW}${EXERCISE_DIFFICULTY}${BLUE}. You can edit this later in the config.json file"
59+
# using default value for author
60+
AUTHOR_HANDLE=${3:-$(get_author_handle)}
61+
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"
62+
63+
64+
create_rust_files "$EXERCISE_DIR" "$SLUG" "$HAS_CANONICAL_DATA"
65+
66+
67+
# ==================================================
68+
69+
# build configlet
70+
./bin/fetch-configlet
71+
message "success" "Fetched configlet successfully!"
72+
73+
# Preparing config.json
74+
message "info" "Adding instructions and configuration files..."
75+
UUID=$(bin/configlet uuid)
76+
77+
jq --arg slug "$SLUG" --arg uuid "$UUID" --arg name "$EXERCISE_NAME" --arg difficulty "$EXERCISE_DIFFICULTY" \
78+
'.exercises.practice += [{slug: $slug, name: $name, uuid: $uuid, practices: [], prerequisites: [], difficulty: $difficulty}]' \
79+
config.json >config.json.tmp
80+
# jq always rounds whole numbers, but average_run_time needs to be a float
81+
sed -i 's/"average_run_time": \([0-9]\+\)$/"average_run_time": \1.0/' config.json.tmp
82+
mv config.json.tmp config.json
83+
message "success" "Added instructions and configuration files"
84+
85+
# Create instructions and config files
86+
echo "Creating instructions and config files"
87+
./bin/configlet sync --update --yes --docs --metadata --exercise "$SLUG"
88+
./bin/configlet sync --update --yes --filepaths --exercise "$SLUG"
89+
./bin/configlet sync --update --tests include --exercise "$SLUG"
90+
message "success" "Created instructions and config files"
91+
92+
META_CONFIG="$EXERCISE_DIR"/.meta/config.json
93+
jq --arg author "$AUTHOR_HANDLE" '.authors += [$author]' "$META_CONFIG" >"$META_CONFIG".tmp && mv "$META_CONFIG".tmp "$META_CONFIG"
94+
message "success" "You've been added as the author of this exercise."
95+
96+
sed -i "s/name = \".*\"/name = \"$UNDERSCORED_SLUG\"/" "$EXERCISE_DIR"/Cargo.toml
97+
98+
message "done" "All stub files were created."
99+
100+
message "info" "After implementing the solution, tests and configuration, please run:"
101+
echo "./bin/configlet fmt --update --yes --exercise ${SLUG}"

bin/fetch_canonical_data

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env bash
2+
# This script fetches the canonical data of the exercise.
3+
4+
if [ $# -ne 1 ]; then
5+
echo "Usage: bin/fetch_canonical_data <exercise-slug>"
6+
exit 1
7+
fi
8+
9+
# check if curl is installed
10+
command -v curl >/dev/null 2>&1 || {
11+
echo >&2 "curl is required but not installed. Please install it and make sure it's in your PATH."
12+
exit 1
13+
}
14+
15+
slug=$1
16+
17+
curlopts=(
18+
--silent
19+
--retry 3
20+
--max-time 4
21+
)
22+
curl "${curlopts[@]}" "https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/${slug}/canonical-data.json"

bin/generate_tests

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
# shellcheck source=/dev/null
5+
source ./bin/generator-utils/utils.sh
6+
7+
function digest_template() {
8+
template=$(cat bin/test_template)
9+
# shellcheck disable=SC2001
10+
# turn every token into a jq command
11+
echo "$template" | sed 's/${\([^}]*\)\}\$/$(echo $case | jq -r '\''.\1'\'')/g'
12+
}
13+
14+
message "info" "Generating tests.."
15+
canonical_json=$(cat canonical_data.json)
16+
SLUG=$(echo "$canonical_json" | jq '.exercise')
17+
# shellcheck disable=SC2001
18+
# Remove double quotes
19+
SLUG=$(echo "$SLUG" | sed 's/"//g')
20+
EXERCISE_DIR="exercises/practice/$SLUG"
21+
TEST_FILE="$EXERCISE_DIR/tests/$SLUG.rs"
22+
23+
cat <<EOT >"$TEST_FILE"
24+
use $(dash_to_underscore "$SLUG")::*;
25+
// Add tests here
26+
27+
EOT
28+
29+
# Flattens canonical json, extracts only the objects with a uuid
30+
cases=$(echo "$canonical_json" | jq '[ .. | objects | with_entries(select(.key | IN("uuid", "description", "input", "expected", "property"))) | select(. != {}) | select(has("uuid")) ]')
31+
32+
# shellcheck disable=SC2034
33+
jq -c '.[]' <<<"$cases" | while read -r case; do
34+
35+
# Evaluate the bash parts and replace them with their return values
36+
eval_template="$(digest_template | sed -e "s/\$(\(.*\))/\$\(\1\)/g")"
37+
eval_template="$(eval "echo \"$eval_template\"")"
38+
39+
# Turn function name unto snake_case
40+
formatted_template=$(echo "$eval_template" | sed -e ':loop' -e 's/\(fn[^(]*\)[ -]/\1_/g' -e 't loop' | sed 's/fn_/fn /')
41+
# Push to file
42+
43+
echo "$formatted_template" >>"$TEST_FILE"
44+
printf "\\n" >>"$TEST_FILE"
45+
46+
done
47+
48+
rustfmt "$TEST_FILE"
49+
50+
message "success" "Generated tests successfully! Check out ${TEST_FILE}"

bin/generator-utils/colors.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env bash
2+
3+
# shellcheck disable=SC2034
4+
# Reset
5+
RESET=$(echo -e '\033[0m')
6+
7+
# Regular Colors
8+
BLACK=$(echo -e '\033[0;30m')
9+
RED=$(echo -e '\033[0;31m')
10+
GREEN=$(echo -e '\033[0;32m')
11+
YELLOW=$(echo -e '\033[0;33m')
12+
BLUE=$(echo -e '\033[0;34m')
13+
PURPLE=$(echo -e '\033[0;35m')
14+
CYAN=$(echo -e '\033[0;36m')
15+
WHITE=$(echo -e '\033[0;37m')
16+
17+
# Bold
18+
BOLD_BLACK=$(echo -e '\033[1;30m')
19+
BOLD_RED=$(echo -e '\033[1;31m')
20+
BOLD_GREEN=$(echo -e '\033[1;32m')
21+
BOLD_YELLOW=$(echo -e '\033[1;33m')
22+
BOLD_BLUE=$(echo -e '\033[1;34m')
23+
BOLD_PURPLE=$(echo -e '\033[1;35m')
24+
BOLD_CYAN=$(echo -e '\033[1;36m')
25+
BOLD_WHITE=$(echo -e '\033[1;37m')
26+
27+
# Underline
28+
UNDERLINE=$(echo -e ='\033[4m')
29+
30+
# Background
31+
BG_BLACK=$(echo -e ='\033[40m')
32+
BG_RED=$(echo -e ='\033[41m')
33+
BG_GREEN=$(echo -e ='\033[42m')
34+
BG_YELLOW=$(echo -e ='\033[43m')
35+
BG_BLUE=$(echo -e ='\033[44m')
36+
BG_PURPLE=$(echo -e ='\033[45m')
37+
BG_CYAN=$(echo -e ='\033[46m')
38+
BG_WHITE=$(echo -e ='\033[47m')

bin/generator-utils/prompts.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env bash
2+
3+
# shellcheck source=/dev/null
4+
source ./bin/generator-utils/colors.sh
5+
6+
function get_exercise_difficulty() {
7+
read -rp "Difficulty of exercise (1-10): " exercise_difficulty
8+
echo "$exercise_difficulty"
9+
}
10+
11+
function validate_difficulty_input() {
12+
13+
valid_input=false
14+
while ! $valid_input; do
15+
if [[ "$1" =~ ^[1-9]$|^10$ ]]; then
16+
exercise_difficulty=$1
17+
valid_input=true
18+
else
19+
read -rp "${RED}Invalid input. ${RESET}Please enter an integer between 1 and 10. " exercise_difficulty
20+
[[ "$exercise_difficulty" =~ ^[1-9]$|^10$ ]] && valid_input=true
21+
22+
fi
23+
done
24+
echo "$exercise_difficulty"
25+
}
26+
27+
function get_author_handle {
28+
DEFAULT_AUTHOR_HANDLE="$(git config user.name)"
29+
30+
if [ -z "$DEFAULT_AUTHOR_HANDLE" ]; then
31+
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
32+
else
33+
AUTHOR_HANDLE="$DEFAULT_AUTHOR_HANDLE"
34+
35+
fi
36+
echo "$AUTHOR_HANDLE"
37+
38+
}

0 commit comments

Comments
 (0)