-
Notifications
You must be signed in to change notification settings - Fork 525
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Add a clap-based CLI All user-input can now be entered upfront with cli flags or later by being prompted for it interactively. - Limit output of rustfmt passed onto user (There are usually many duplicates when generating test cases.) - Handle absence of canonical data (e.g. for custom exercises) not from problem-specifications) piggyback: - Make exercise difficulty in track config type-safe
Showing
15 changed files
with
487 additions
and
267 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
[workspace] | ||
members = ["generator", "ci-tests", "utils", "models"] | ||
members = ["generate", "ci-tests", "utils", "models"] | ||
resolver = "2" |
This file was deleted.
Oops, something went wrong.
3 changes: 2 additions & 1 deletion
3
rust-tooling/generator/Cargo.toml → rust-tooling/generate/Cargo.toml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
use clap::{Parser, Subcommand}; | ||
use convert_case::{Case, Casing}; | ||
use glob::glob; | ||
use inquire::{validator::Validation, Select, Text}; | ||
use models::track_config; | ||
|
||
#[derive(Parser)] | ||
#[command(author, version, about, long_about = None)] | ||
pub struct CliArgs { | ||
#[command(subcommand)] | ||
pub command: Command, | ||
} | ||
|
||
#[derive(Subcommand)] | ||
pub enum Command { | ||
Add(AddArgs), | ||
Update(UpdateArgs), | ||
} | ||
|
||
impl std::fmt::Display for Command { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
match self { | ||
Command::Add(_) => write!(f, "Add"), | ||
Command::Update(_) => write!(f, "Update"), | ||
} | ||
} | ||
} | ||
|
||
#[derive(Parser)] | ||
pub struct AddArgs { | ||
#[arg(short, long)] | ||
slug: Option<String>, | ||
|
||
#[arg(short, long)] | ||
name: Option<String>, | ||
|
||
#[arg(short, long)] | ||
difficulty: Option<Difficulty>, | ||
} | ||
|
||
pub struct FullAddArgs { | ||
pub slug: String, | ||
pub name: String, | ||
pub difficulty: track_config::Difficulty, | ||
} | ||
|
||
impl AddArgs { | ||
pub fn unwrap_args_or_prompt(self) -> FullAddArgs { | ||
let slug = self.slug.unwrap_or_else(prompt_for_add_slug); | ||
let name = self.name.unwrap_or_else(|| prompt_for_exercise_name(&slug)); | ||
let difficulty = self.difficulty.unwrap_or_else(prompt_for_difficulty).into(); | ||
FullAddArgs { | ||
slug, | ||
name, | ||
difficulty, | ||
} | ||
} | ||
} | ||
|
||
#[derive(Parser)] | ||
pub struct UpdateArgs { | ||
/// slug of the exercise to update | ||
#[arg(short, long)] | ||
slug: Option<String>, | ||
} | ||
|
||
impl UpdateArgs { | ||
pub fn unwrap_slug_or_prompt(self) -> String { | ||
self.slug.unwrap_or_else(prompt_for_update_slug) | ||
} | ||
} | ||
|
||
pub fn prompt_for_update_slug() -> String { | ||
let implemented_exercises = glob("exercises/practice/*") | ||
.unwrap() | ||
.filter_map(Result::ok) | ||
.map(|path| path.file_name().unwrap().to_str().unwrap().to_string()) | ||
.collect::<Vec<_>>(); | ||
|
||
Select::new( | ||
"Which exercise would you like to update?", | ||
implemented_exercises, | ||
) | ||
.prompt() | ||
.unwrap() | ||
} | ||
|
||
pub fn prompt_for_add_slug() -> 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::<Vec<_>>(); | ||
|
||
let todo_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::<Vec<_>>(); | ||
|
||
println!("(suggestions are from problem-specifications)"); | ||
Text::new("What's the slug of your exercise?") | ||
.with_autocomplete(move |input: &_| { | ||
let mut slugs = todo_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() | ||
} | ||
|
||
pub fn prompt_for_exercise_name(slug: &str) -> String { | ||
Text::new("What's the name of your exercise?") | ||
.with_initial_value(&slug.to_case(Case::Title)) | ||
.prompt() | ||
.unwrap() | ||
} | ||
|
||
/// Mostly a clone of the `Difficulty` enum from `models::track_config`. | ||
/// The purpose of this is that we can implement cli-specific traits in this crate. | ||
#[derive(Debug, Clone, Copy, clap::ValueEnum)] | ||
#[repr(u8)] | ||
pub enum Difficulty { | ||
Easy = 1, | ||
Medium = 4, | ||
// I'm not sure why there are two medium difficulties | ||
Medium2 = 7, | ||
Hard = 10, | ||
} | ||
|
||
impl From<Difficulty> for track_config::Difficulty { | ||
fn from(value: Difficulty) -> Self { | ||
match value { | ||
Difficulty::Easy => track_config::Difficulty::Easy, | ||
Difficulty::Medium => track_config::Difficulty::Medium, | ||
Difficulty::Medium2 => track_config::Difficulty::Medium2, | ||
Difficulty::Hard => track_config::Difficulty::Hard, | ||
} | ||
} | ||
} | ||
|
||
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)"), | ||
} | ||
} | ||
} | ||
|
||
pub fn prompt_for_difficulty() -> Difficulty { | ||
Select::new( | ||
"What's the difficulty of your exercise?", | ||
vec![ | ||
Difficulty::Easy, | ||
Difficulty::Medium, | ||
Difficulty::Medium2, | ||
Difficulty::Hard, | ||
], | ||
) | ||
.prompt() | ||
.unwrap() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
use std::path::PathBuf; | ||
|
||
use clap::Parser; | ||
use cli::{AddArgs, FullAddArgs, UpdateArgs}; | ||
use models::track_config::{self, TRACK_CONFIG}; | ||
|
||
mod cli; | ||
mod exercise_generation; | ||
|
||
fn main() { | ||
utils::fs::cd_into_repo_root(); | ||
|
||
let cli_args = cli::CliArgs::parse(); | ||
|
||
match cli_args.command { | ||
cli::Command::Add(args) => add_exercise(args), | ||
cli::Command::Update(args) => update_exercise(args), | ||
} | ||
} | ||
|
||
fn add_exercise(args: AddArgs) { | ||
let FullAddArgs { | ||
slug, | ||
name, | ||
difficulty, | ||
} = args.unwrap_args_or_prompt(); | ||
|
||
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." | ||
); | ||
|
||
make_configlet_generate_what_it_can(&slug); | ||
|
||
let is_update = false; | ||
generate_exercise_files(&slug, is_update); | ||
} | ||
|
||
fn update_exercise(args: UpdateArgs) { | ||
let slug = args.unwrap_slug_or_prompt(); | ||
|
||
make_configlet_generate_what_it_can(&slug); | ||
|
||
let is_update = true; | ||
generate_exercise_files(&slug, is_update); | ||
} | ||
|
||
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<String> { | ||
let lib_rs = | ||
std::fs::read_to_string(format!("exercises/practice/{}/src/lib.rs", slug)).unwrap(); | ||
|
||
lib_rs | ||
.split("fn ") | ||
.skip(1) | ||
.map(|f| { | ||
let tmp = f.split_once('(').unwrap().0; | ||
// strip generics | ||
if let Some((res, _)) = tmp.split_once('<') { | ||
res.to_string() | ||
} else { | ||
tmp.to_string() | ||
} | ||
}) | ||
.collect() | ||
} |
File renamed without changes.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters